Node.js 101 : Part #3 – A Basic API

Following on from my recent post about doing something this year, I’m committing to doing 12 months of “101”s; posts and projects themed at begining something new (or reasonably new) to me.

January is all about node, and I started with a basic intro, then cracked open a basic web server with content-type manipulation and basic routing.

Building and calling an API in node

Now on to the meat of this month; building a basic RESTful API. I don’t plan on writing the underlying business logic myself, so will just wrap an existing API in order to further demonstrate the routing, content type usage, and proxying calls to another server.

For this post I’ll be using the Asos API for querying the Asos database of clothes and returning the data necessary to build other basic applications on; intially a web site, but later on various apps on various devices.

The Underlying API: Asos.com

Asos, the online fashion “destination”, had an API open for developers to mess aorund with for a short period and as one of the first people to get involved I managed to snap up an api key. This will give me the ability to query the product catalogue and do basic functions such as adding products to a basket.

Asos
asos-1

Asos API
asos-api-1

An example request takes the format:

http://api1.asos.com/product/{productId}/{locale}/{currency}?api_key={apiKey}

and an example response is:

{
   "BasePrice":35.0,
   "Brand":"ASOS",
   "Colour":null,
   "CurrentPrice":"£35.00",
   "InStock":true,
   "IsInSet":false,
   "PreviousPrice":"",
   "PriceType":"Full",
   "ProductId":1703489,
   "ProductImageUrls":[
      "http://images.asos.com/inv/media/9/8/4/3/1703489/red/image1xxl.jpg",
      "http://images.asos.com/inv/media/9/8/4/3/1703489/image2xxl.jpg",
      "http://images.asos.com/inv/media/9/8/4/3/1703489/image3xxl.jpg",
      "http://images.asos.com/inv/media/9/8/4/3/1703489/image4xxl.jpg"
   ],
   "RRP":"",
   "Size":null,
   "Sku":"101050",
   "Title":"ASOS Fringe Sleeve Mesh Crop",
   "AdditionalInfo":"100% Polyester\n\n\n\n\n\nSIZE & FIT \n\nModel wears: UK 8/ EU 36/ US 4\n\n\n\nSize UK 8/ EU 36/ US 4 side neck to hem measures: 46cm/18in",
   "AssociatedProducts":[{
         "BasePrice":35.0,
         "Brand":"ASOS",
         "Colour":null,
         "CurrentPrice":"£35.00",
         "InStock":false,
         "IsInSet":false,
         "PreviousPrice":"",
         "PriceType":"Full",
         "ProductId":1645550,
         "ProductImageUrls":[
            "http://images.asos.com/inv/media/0/5/5/5/1645550/black/image1l.jpg"
         ],
         "RRP":"",
         "Size":null,
         "Sku":null,
         "Title":"ASOS Panel Mesh Body Contour Top",
         "ProductType":"Recommendations"
      }],
   "CareInfo":"Machine wash according to instructions on care label",
   "Description":"Fringed crop top, featuring a reinforced boat neckline, raglan style slashed sleeves with tasselled fringe trim, and a cropped length, in a sheer finish.",
   "Variants":[
      {
         "BasePrice":35.00,
         "Brand":null,
         "Colour":"Beige",
         "CurrentPrice":"£35.00",
         "InStock":true,
         "IsInSet":false,
         "PreviousPrice":"",
         "PriceType":"Full",
         "ProductId":1716611,
         "ProductImageUrls":[
            "http://images.asos.com//inv/media/9/8/4/3/1703489/beige/image1xxl.jpg"
         ],
         "RRP":"",
         "Size":"UK 6",
         "Sku":null,
         "Title":null
      }]
}

For the purposes of this post all I want to do is wrap a couple of the slightly confusing and overly complex Asos API calls with some really basic, more RESTy, ones.

To do this I’m going to initially create a new module called:

proxy.js

var http = require('http');

function getRemoteData(host, requestPath, callback){
    var options = {
        host: host,
        port: 80,
        path: requestPath
    };
 
    var buffer = '';
    var request = http.get(options, function(result){
        result.setEncoding('utf8');
        result.on('data', function(chunk){
            buffer += chunk;
        });
        result.on('end', function(){
            callback(buffer);
        });
    });
    request.on('error', function(e){console.log('error from proxy call: ' + e.message)});
    request.end();
};

exports.getRemoteData=getRemoteData;

As you can see, all this does is make an HTTP GET call to a remote server, passing the “options” object.

Using the “on” event wiring up notation, I’ve just appended the chunks of data returned from the GET call to a variable, which is then passed to the referenced callback function.

Now I’ll wire this up:
requestHandlers.js:

var proxy = require('./proxy');

function products(response) {
console.log("Request handler 'products' was called");

  var host = 'api1.asos.com';
  var requestPath = '/productlisting/search/jeans/1/PriceAscending/en_API/GBP?api_key={snipped api key}';
  
  response.writeHead(200, {"Content-Type": "application/json"});

  proxy.getRemoteData(host, requestPath, function(json){
  response.write(json);
    response.end();
  });  
}

exports.products = products;

I’m removing the previously entered hello, goodbye, and favicon routes for brevity. Notice the reference to the proxy module at the top as well as the new handler itself.

The URL used above executes a product search for the term “jeans”.

Wire it all up:
server.js:

var http = require("http"),
    url = require("url");

function start(route, handle, port) {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    route(handle, pathname, response);
  }

http.createServer(onRequest).listen(port);
  console.log("Server has started listening on port " + port);
}

exports.start = start;

app.js

var server = require("./server"),
  router = require("./route"),
  requestHandlers = require("./requestHandlers");

var handle = {}
handle["/products"] = requestHandlers.products

var port = process.env.PORT || 3000;
server.start(router.route, handle, port);

Kick off

nodemon app.js

If you were to have an API key and had put it in the URL above, you’d see something like:

asos-products-1

Right. Ok. That’s a lot of data. Just for now I’d like to make it easier to view, so I’ll limit what is returned and also just write out a basic HTML page.

requestHandlers.js:

var proxy = require('./proxy');

function products(response) {
console.log("Request handler 'products' was called");

  var host = 'api1.asos.com';
  var requestPath = '/productlisting/search/jeans/1/PriceAscending/en_API/GBP?api_key={snipped api key}';
  response.writeHead(200, {"Content-Type": "text/html"});

  proxy.getRemoteData(host, requestPath, function(json){
    var data = JSON.parse(json);

  var html = "<h1>Asos Search for JEANS</h1>";
  response.write(html);

    for(var i=0; i<data.ItemCount; i++) {
      if (data.Listings[i] != null){
      response.write("<li>"
       + data.Listings[i].Title + "<br /><img src='" 
       + data.Listings[i].ProductImageUrl + "' /></li>");
      }
    }

    response.end();
  });  
}

exports.products = products;

Given that the Asos Api returns valid JSON I can just parse it and then access the structure of that JSON; in this case the ItemCount & Listings at the top level and Title & ProductImageUrl within Listings.

This will now display something like:
asos-products-2

(Really? A beanie is the first result in the search for “jeans”? Anyway…)

Actually searching

Next we’ll just make the request actually execute a search with the value passed in to our own API, using the format “/products/{search term}”

Firstly I’ll edit the router to take the primary route handler from the first part of the URL (e.g “http://localhost:3000/products/jeans”) and pass the full path into the router for further use.

router.js:

function route(handle, pathname, response) {
  var root = pathname.split('/')[1];

  if (typeof handle[root] === 'function') {
    handle[root](response, pathname);
  } else {
    console.log("No request handler found for " + pathname);
    response.writeHead(404, {"Content-Type": "text/plain"});
    response.write("404 Not found");
    response.end();
  }
}

exports.route = route;

Next change the request handler to pick out the next section from the url e.g. “http://localhost:3000/products/jeans

requestHandlers.js:

var proxy = require('./proxy');

function products(response) {
console.log("Request handler 'products' was called");

  var search = path.split('/')[2];
  var host = 'api1.asos.com';
  var requestPath = '/productlisting/search/' + search + '/1/PriceAscending/en_API/GBP?api_key={snipped api key}';
  
  response.writeHead(200, {"Content-Type": "text/html"});

  proxy.getRemoteData(host, requestPath, function(json){
    var data = JSON.parse(json);

  var html = "<h1>Asos Search for " + search + "</h1>";
  response.write(html);

    for(var i=0; i<data.ItemCount; i++) {
      if (data.Listings[i] != null){
      response.write("<li>"
       + data.Listings[i].Title + "<br /><img src='" 
       + data.Listings[i].ProductImageUrl + "' /></li>");
      }
    }

    response.end();
  });  
}

exports.products = products;

One last tweak to the initialisation file to remove a leading slash which isn’t needed now that we’re splitting the url to match instead of using the full url path:

app.js:

var server = require("./server"),
  router = require("./router"),
  requestHandlers = require("./requestHandlers");

var handle = {}
handle["products"] = requestHandlers.products;

var port = process.env.PORT || 3000;
server.start(router.route, handle, port);

We now have basic search capabilities:
asos-products-search-1

Now let’s get a basic product detail page working. For this I should need to just add a new request handler and wire it up.

requestHandlers.js:

var proxy = require('./proxy');

function products(response, path) {
console.log("Request handler 'products' was called");

  var search = path.split('/')[2];
  var host = 'api1.asos.com';
  var requestPath = '/productlisting/search/' + search + '/1/PriceAscending/en_API/GBP?api_key={snipped api key}';
  
  response.writeHead(200, {"Content-Type": "text/html"});

  proxy.getRemoteData(host, requestPath, function(json){
    var data = JSON.parse(json);

  var html = "<h1>Asos Search for " + search + "</h1>";
  response.write(html);

    for(var i=0; i<data.ItemCount; i++) {
      if (data.Listings[i] != null){
      response.write("<li><a href='/product/" + data.Listings[i].ProductId + "'>"
       + data.Listings[i].Title + "<br /><img src='" 
       + data.Listings[i].ProductImageUrl + "' /></a></li>");
      }
    }

    response.end();
  });  
}

function product(response, path) {
  console.log("Request handler 'product' was called for " + path);

  var productId = path.split('/')[2];
  var host = 'api1.asos.com';
  var requestPath = '/product/' + productId + '/en_API/GBP?api_key={snipped api key}';

  response.writeHead(200, {"Content-Type": "text/html"});
  proxy.getRemoteData(host, requestPath, function(json){
  var data = JSON.parse(json);

    var html = "<h1>" + data.Title + "</h1>"
    + "<img src='" + data.ProductImageUrls[0].replace('xxl','xl') + "' />"
    response.write(html);
    response.end();
  });  
}
exports.products = products;
exports.product = product;

As well as the new handler I’ve also added a link from the listing page to the detail page, just for FUN.

app.js:

var server = require("./server"),
  router = require("./router"),
  requestHandlers = require("./requestHandlers");

var handle = {}
handle["products"] = requestHandlers.products;
handle["product"] = requestHandlers.product;

var port = process.env.PORT || 3000;
server.start(router.route, handle, port);

asos-product-1

Back to JSON

Ok, so that’s a very basic website wrapped around an API. Since I plan to use this wrapper as a basic API itself I’m going to revert it to returning JSON and simplify the data structure for my needs.

requestHandlers.js:

var proxy = require('./proxy');

function products(response, path) {
console.log("Request handler 'products' was called");

  var search = path.split('/')[2];
  var host = 'api1.asos.com';
  var requestPath = '/productlisting/search/' + search + '/1/PriceAscending/en_API/GBP?api_key={stripped api key}';
  
  response.writeHead(200, {"Content-Type": "application/json"});

  proxy.getRemoteData(host, requestPath, function(json){
    var data = JSON.parse(json);

  var newJson = {
    category: data.Description,
    products: []
  };

  data.Listings.forEach(function(listing){
      newJson.products.push({
        id: listing.ProductId,
        title: listing.Title,
        price: listing.CurrentPrice,
        image: listing.ProductImageUrl[0]
      })
    });

    response.write(JSON.stringify(newJson));
    response.end();
  });  
}

function product(response, path) {
  console.log("Request handler 'product' was called for " + path);

  var productId = path.split('/')[2];
  var host = 'api1.asos.com';
  var requestPath = '/product/' + productId + '/en_API/GBP?api_key={snipped api key}';

  response.writeHead(200, {"Content-Type": "application/json"});
  proxy.getRemoteData(host, requestPath, function(json){
  var data = JSON.parse(json);

    var newJson = {
      id: data.ProductId,
      title: data.Title,
      price: data.CurrentPrice,
      available: data.InStock,
      image: data.ProductImageUrls[0]
    };

    response.write(JSON.stringify(newJson));
    response.end();
  });  
}
exports.products = products;
exports.product = product;

Which ends up looking like:
asos-json-1

That’ll do me for now, even though it would be nice to abstract the mapping out somewhere else. Out of scope for me at the moment though.

Once last thing for this post:

Passing in command line arguments

Throughout this post I’ve been diligently snipping out my API key before pasting the code in. There are many approaches to dev/qa/staging/production configuration management (some as basic as a text file, some a bit more complex) which would handle this sort of thing but for my immediate requirements I will just pass the API key in as a command line argument.

To handle this I need to edit the initialisation code in order to pick up any args passed, and documented on the nodejs.org site:

app.js:

var server = require("./server"),
  router = require("./router"),
  requestHandlers = require("./requestHandlers");

var handle = {}
handle["products"] = requestHandlers.products;
handle["product"] = requestHandlers.product;

var apiKey = process.argv[2];
var port = process.env.PORT || 3000;
server.start(router.route, handle, port, apiKey);

Now just pass that value around the rest of the system:

server.js:

var http = require("http"),
  url = require("url");

function start(route, handle, port, apiKey) {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    route(handle, pathname, response, apiKey);
  }

  http.createServer(onRequest).listen(port);
  console.log("Server has started listening on port " + port);
}

exports.start = start;

route.js:

function route(handle, pathname, response, apiKey) {
  var root = pathname.split('/')[1];

  if (typeof handle[root] === 'function') {
    handle[root](response, pathname, apiKey);
  } else {
    console.log("No request handler found for " + pathname + " (" + root+ ")");
    response.writeHead(404, {"Content-Type": "text/plain"});
    response.write("404 Not found");
    response.end();
  }
}

exports.route = route;

requestHandlers.js:

var proxy = require('./proxy');

function products(response, path, apiKey) {
console.log("Request handler 'products' was called");

  var search = path.split('/')[2];
  var host = 'api1.asos.com';
  var requestPath = '/productlisting/search/' + search + '/1/PriceAscending/en_API/GBP?api_key=' + apiKey;
  
  response.writeHead(200, {"Content-Type": "application/json"});

  proxy.getRemoteData(host, requestPath, function(json){
    var data = JSON.parse(json);

  var newJson = {
    category: data.Description,
    products: []
  };

  data.Listings.forEach(function(listing){
      newJson.products.push({
        id: listing.ProductId,
        title: listing.Title,
        price: listing.CurrentPrice,
        image: listing.ProductImageUrl[0]
      })
    });

    response.write(JSON.stringify(newJson));
    response.end();
  });  
}

function product(response, path, apiKey) {
  console.log("Request handler 'product' was called for " + path);

  var productId = path.split('/')[2];
  var host = 'api1.asos.com';
  var requestPath = '/product/' + productId + '/en_API/GBP?api_key=' + apiKey;

  response.writeHead(200, {"Content-Type": "application/json"});
  proxy.getRemoteData(host, requestPath, function(json){
  var data = JSON.parse(json);

    var newJson = {
      id: data.ProductId,
      title: data.Title,
      price: data.CurrentPrice,
      available: data.InStock,
      image: data.ProductImageUrls[0]
    };

    response.write(JSON.stringify(newJson));
    response.end();
  });  
}
exports.products = products;
exports.product = product;

Then to pass in the api key just change the nodemon call to

nodemon app.js myApIK3y

The files for this post can be found over on github

Coming up

The next post this month will cover some nice deployment & hosting options for node!

Scripting the setup of a developer PC, Part 4 of 4 – Installing Custom Stuff, Interesting Things Encountered, and Conclusion

This is the final part of a four part series on attempting to automate installation and setup of a development PC with a few scripts and some funky tools. If you haven’t already, why not read the introductory post about ninite, the second part about the command line version of WebPI or perhaps the third instalment about the interesting chocolatey project? Disclaimer: this series was inspired by a blog from Maarten Balliauw

Installing Custom Stuff

There are some other applications out there which I need to be able to install via script as well, such as SQL Server 2008 R2 Tools; although WebPI (and with Chocolately Beta) can install SQL Tools, you’re actually limited to the express versions (AFAIK), and I need the standard install.

Since I have the ISO for this on the network, I can run virtualclonedrive from commandline (after chocolatey installs it) to mount the iso and run the setup application using “vcdmount.exe /l=<drive letter> <iso path>”.

Execute VCDMount with no params to get this helpful dialog for other command line params:

image

So let’s get on with it then:

SQL Server 2008 R2 Tools

It looks like SQL Server has its own command line install options; if I mount the network ISO and pass the parameters “/ACTION=install /IACCEPTSQLSERVERLICENSETERMS /Q /FEATURES=Tools,ADV_SSMS” I should be able to install SQL tools unattended. There is a dependency on Windows Installer 4.5 being installed correctly for this one to work; make sure your WebPI install worked earlier!

 

Visual Studio 2010

It looks like VS2010 has its own command line install options; if I mount the network ISO and pass the parameters “/q /full /norestart” I should be able to install VS2010 unattended. There is an entry for “VS2010SP1Core” in the WebPI xml feeds, and I have tried using that to no avail; see “Interesting Things Encountered” section at the end for a note about WebPI & VS2010.

So the final install script should look like:

@echo off

REM VS2010
"c:\Program Files (x86)\Elaborate Bytes\VirtualCloneDrive\vcdmount.exe" /l=E "Z:\Installation\SetupDevPC\VS2010SP1.ISO" 

E:/Setup/setup.exe /q /full /norestart

REM SQL Tools
"c:\Program Files (x86)\Elaborate Bytes\VirtualCloneDrive\vcdmount.exe" /l=E "Z:\Installation\SetupDevPC\SQLServer2008Tools.ISO"

E:/setup.exe /ACTION=install /IACCEPTSQLSERVERLICENSETERMS /Q /FEATURES=Tools,ADV_SSMS

Something to bear in mind is that this doesn’t work if you haven’t restarted since running the chocolatey powershell script. As such, I’ve edited the chocolatey powershell script to end with:

shutdown /r /t 0 /d P:0:0

If all goes well you shouldn’t actually see anything of note; VirtualCloneDrive’s VCDMount mounts each ISO into drive “E” (VCD default install has only one virtual drive defined, in my case that was “E”) and calls the relevant executable with parameters to attempt to force a silent install. VS2010 is completely silent! SQL at least gives a few lines of feedback.

The Bad News

Unfortunately VS2010’s setup.exe doesn’t wait before returning to the script; as such, you would see the call to VS2010’s setup.exe kick off then a few seconds later a call to SQL2008’s setup.exe, which fails since there’s another install already happening.

Again, just as unfortunately, SQL2008 won’t install straight after VS2010 – it demands a restart.

The “Meh” News

My preference is now to install SQL2008 first, since this is a blocking process, then VS2010, then let it restart (remove the “/norestart” flag for VS2010).

Hence the last script is actually:

@echo off

REM SQL Tools
"c:\Program Files (x86)\Elaborate Bytes\VirtualCloneDrive\vcdmount.exe" /l=E "Z:\Installation\SetupDevPC\SQLServer2008Tools.ISO"

E:/setup.exe /ACTION=install /IACCEPTSQLSERVERLICENSETERMS /Q /FEATURES=Tools,ADV_SSMS

REM VS2010
"c:\Program Files (x86)\Elaborate Bytes\VirtualCloneDrive\vcdmount.exe" /l=E "Z:\Installation\SetupDevPC\VS2010SP1.ISO" 

E:/Setup/setup.exe /q /full

Along with the previous powershell script and the beta chocolatey nupkg, the existing script for ninite and webpi and their components, the final directory contents now look like:

281211_autoinstall_iso_dir_contents

The End Result

The Result!

Which brings us FINALLY on to the:

Conclusion

Although it is entirely possible to script the setup of a develop PC without requiring ever seeing a GUI currently, using the tools I’ve chosen to use here, it seems that it can’t be done in a fully automated fashion. Certain products still popped up a confirmation dialog, others required a reboot when I’d specifically suppressed one. Some dependencies were not always resolved correctly.

As such, I hope that you have enjoyed this introduction into my attempt to teach myself some command line WebPI, ninite, chocolatey, and general hackery, and if you have any comments or suggestions please feel free to let me know in the comments or via twitter; I’ve kept various snapshots of my VM I used for this series, so I’ll happily try out any good suggestions!

It would appear that this is a nice set of basic scripts to get a development PC up and running, however once this has been done it makes much more sense to create an image and use that for future setups. There will be a follow up post about creating an image of this configured PC so that future developer PCs can use the image instead of having to reinstall everything, which should be a pretty basic one since it’s nothing new!

Finally, these articles are already out of date! WebPI is now on v4 and the chocolatey “beta” I mentioned is actually now the mainline. No doubt everything else will be out of date in a few more days.

Interesting Things Encountered

  • The webpicmdline tool still raises the odd dialog prompting for a restart (e.g. for MVC3), even with the “suppressreboot” option. Using a really loooong product list I’d specified in one webpi command fails for a lot of them. After rebooting, webpicmd didn’t automatically pick up and carry on as expected; this is why I’ve cut the initial webpi product list to a small number and done the others via chocolatey.

  • Webpicmdline doesn’t install things in the order you list them, which can be a bit odd. i.e., WindowsInstaller45 attempts to install after .Net 4 and promptly fails. Do it on its own and you’re fine.

  • Chocolatey’s webpi support didn’t initially work; I had to restart before I could install anything. I believe this to be related to the webpi installation of WindowsInstaller45 whose required reboot I had suppressed.

  • VS2010’s “/q /full” setup options are incredibly “q” – nothing appears at all; no command line feedback, no GUI. I had to fire off setup.exe without params just to see the GUI load and show me it’s already halfway through the install process! Fantastic.

  • VS2010 exists within the WebPI listing as “VS2010SP1Core” but seems to always fail with an error about needing “VS2010SP1Prerequisite”; this product also exists in the same WebPI feed but was always failing to install via webpicmdline and chocolatey for me. Let me know if you get this working!

The Resulting Scripts

Setup_Step1.cmd

Ninite & WebPI

@echo off

REM Ninite stuff
cmd /C "Z:\Installation\SetupDevPC\Ninite_DevPC_Utils.exe"

REM WebPI stuff
cmd /C "Z:\Installation\SetupDevPC\webpicmdline.exe /AcceptEula /SuppressReboot /Products:PowerShell,PowerShell2,NETFramework20SP2,NETFramework35,NETFramework4"

cmd /C "Z:\Installation\SetupDevPC\webpicmdline.exe /AcceptEula /SuppressReboot /Products:WindowsInstaller31,WindowsInstaller45"

shutdown /r /t 0 /d P:0:0

Setup_Step2.ps1

Chocolatey

# Chocolatey
iex ((new-object net.webclient).DownloadString('http://bit.ly/psChocInstall'))

# install applications
cinst virtualclonedrive
cinst sysinternals
cinst msysgit
cinst fiddler
cinst tortoisesvn

# getting the latest build for webpi support: git clone git://github.com/chocolatey/chocolatey.git | cd chocolatey | build | cd _{tab}| cinst chocolatey -source %cd%
# I’ve already done this and the resulting nugetpkg is also saved in the same network directory:
cinst chocolatey –source "Z:\Installation\SetupDevPC\"

# Now I’ve got choc I may as well use it to install a bunch of other stuff from WebPI;
# things that didn’t always work when I put them in the looong list of comma delimited installs
# IIS
cinst IIS7 -source webpi
cinst ASPNET -source webpi
cinst BasicAuthentication -source webpi
cinst DefaultDocument -source webpi
cinst DigestAuthentication -source webpi
cinst DirectoryBrowse -source webpi
cinst HTTPErrors -source webpi
cinst HTTPLogging -source webpi
cinst HTTPRedirection -source webpi
cinst IIS7_ExtensionLessURLs -source webpi
cinst IISManagementConsole -source webpi
cinst IPSecurity -source webpi
cinst ISAPIExtensions -source webpi
cinst ISAPIFilters -source webpi
cinst LoggingTools -source webpi
cinst MetabaseAndIIS6Compatibility -source webpi
cinst NETExtensibility -source webpi
cinst RequestFiltering -source webpi
cinst RequestMonitor -source webpi
cinst StaticContent -source webpi
cinst StaticContentCompression -source webpi
cinst Tracing -source webpi
cinst WindowsAuthentication -source webpi

shutdown /r /t 0 /d P:0:0

Setup_Step3.cmd

VCDMount

@echo off

REM SQL Tools
"c:\Program Files (x86)\Elaborate Bytes\VirtualCloneDrive\vcdmount.exe" /l=E "Z:\Installation\SetupDevPC\SQLServer2008Tools.ISO"

E:/setup.exe /ACTION=install /IACCEPTSQLSERVERLICENSETERMS /Q /FEATURES=Tools,ADV_SSMS

REM VS2010
"c:\Program Files (x86)\Elaborate Bytes\VirtualCloneDrive\vcdmount.exe" /l=E "Z:\Installation\SetupDevPC\VS2010SP1.ISO"

E:/Setup/setup.exe /q /full

Hope you enjoyed the articles, any feedback is appreciated.