Introduction
When I got my Azure subscription, the first thing I was interested in was creating a VM somewhere else then in our data centers in Amsterdam and better yet in every available Azure data center in the world! This is not easy task to do via the portal or even with Azure PowerShell. Both requires you to setup the storage accounts and VM’s in every Azure region which involves a lot of work and administration. And of course as always my immediate thought was, let’s work it out and do that with some clever PowerShell automation. Little did I know that this project would take a while because of more and more requirements/features (and the interaction between them) were added along the way.
This finally resulted in a PowerShell tool/script which will let you batch provision and manage Azure VM’s within Microsoft-managed datacenters in thirteen regions around the world. All thirteen VM’s are ready to use within an astonishing ten minutes, go Azure PowerShell Automation! I tried to follow the latest PowerShell recommended practices and only abused ‘write-host’ for demo screen output interaction purposes. Security and validation are treated with the utmost regard. Therefore every change or high impact operation is tested, assessed and reviewed, thanks go out to Darryl van der Peijl.
Oh and did I already say that its now quite easy to register an Azure trial within five minutes, that said, you could start your very own CDN in just 15 minutes J
Demo bits
I posted a animated gif above of some of the key parts of the demo to immediately point out what it exactly is and does and of course to get you excited enough to read on J If you just want to see the demo in action without any hassle than the YouTube clip link below is a good representation. VM creation speed may vary from machine to machine and performance of Azure at that moment. Read further down this post to get some more details to use the script and get familiar with the inner workings of it. Please let me know if you found a bug or have an improvement. Also feel free to use pieces of the code and experiment for yourself.
Script: https://gallery.technet.microsoft.com/Azure-Demo-Batch-Provision-b98c75db
Demo screencast: https://www.youtube.com/watch?v=mRkJmgg9IJ0
Execute script: .\AzureDemo_ProvisionVMsGlobally.ps1
Help?: Get-Help .\AzureDemo_ProvisionVMsGlobally.ps1 -Full
Prerequisites
Of course you need to sign up with Azure, Azure offers a one month free trial in which you can spend €150 or $200 on all services. So you really want to keep your VM instance size ‘Small’ (script default) if you plan to keep the VM’s running for a while.
Once you have a subscription you only need to download and install the Azure PowerShell Module which is officially maintained and available on Github. I recommend to download from over there, it’s a standalone package and only contains the module instead of a full Microsoft Web Platform Installer package containing all Azure software and all their dependencies. Microsoft offers the full package for download here.
Experiencing some difficulties with the demo or want to test drive Azure for yourself then please follow the Azure PowerShell documentation here, it contains helpful insight to connect to Azure and get you started.
Please be aware of the Azure VM Core and Cloud Service limit in Azure (default 20), open up a support ticket with Microsoft when you get a PowerShell error about exceeding a limit. They will raise the limit dependent on your Azure service spending needs.
Functionality
Basics
This demo automatically deploys an Azure VM, Azure Service and related Storage Account in each available Microsoft Azure Datacenter. It also facilitates in the provisioning and management of these VM’s through the script advanced functions which are made available in the ‘Global’ scope and thus can be used in the console. This includes the VM and storage account provisioning, remote PowerShell session setup, PowerShell remote command execution, rdp connection and public endpoint update functions. This way it’s fairly easy to provision the VM’s in every Azure Datacenter available, use the resources (network/compute/storage) at that location, connect and execute any remote PowerShell commands.
Script input
All script parameters are optional and will be determined automatically when left blank, one of the goals was to keep user supplied information to a minimum. In addition to the configurable script parameters, the script’s main routine will prompt for the administrator credentials, the Windows image file (deployment) and in which region/datacenter the VM’s should be deployed. The VM and Storage Account names will form a name based on a unique prefix which you can supply as a script parameter. If you don’t supply one ‘get-random’ is used to generate three numbers combined with DEMO in the string as a prefix. This is because the DNS name must be unique in Azure’s public cloud DNS namespace, *.cloudapp.net. I will try to separate the VM name and DNS name in the next version so you could name the VM’s whatever you want. See the example below where all script parameters are used.
1 |
.\AzureDemo_ProvisionVMsGlobally.ps1 -SubscriptionName 'Enterprise' -InstanceSize 'Standard_D1' -VMprefix 'TP2VM-' -StorageAccountPrefix 'tp2st' -ImportPubFile -AddSubscription |
ImportPubFile switch is used to use the Azure certificate import cmdlet ‘Import-AzurePublishSettingsFile’ instead of the ‘Add-AzureAccount’ cmdlet method which expires after 12 hours. The ‘AddSubscription’ switch is used to add a subscription to work with when you already have a existing one.
Console over GUI
The purpose of executing the script cmdlet based functions in the console instead of a GUI is to get acquainted with Azure PowerShell and play around with the functions or the Azure module cmdlets yourself. Everybody can click a cool and nifty IaaS solution together in the Azure portal, but what if something happens or you want to scale up? Did you document well enough and how fast do you have everything online again. That’s something to think about and what better way than do that with some cool Azure PowerShell automation!
More cool Azure PS stuff
Before you choose to clean up the demo with the ‘Cleanup-Demo’ function I highly recommend to use Mark Scholman’s Azure IaaS Toolkit to manage networking, load-balancing and firewalling between the VM’s. Dependent on the feedback I might try to integrate the script functions and main routine in a cool graphical interface from Sapien PowerShell Studio like Mark Scholman did. And to take the whole thing a step further what about integrating Azure’s Desired State Configuration or one even bigger step let’s throw Azure Resource Manager in the mix!
Nano on Azure
The only thing I’m really missing in this demo where I unfortunately have no control over is deploying the much anticipated Nano server Cloud OS. Windows Server Technical Preview 2 is already available to deploy and already a treat to test all the awesome announced new features on but of course we want to test/war drive this Cloud OS on Microsofts own Cloud Platform Azure and see how it performs and consumes. So when it’s out you’re also able to provision and manage Nano with this script.
Under the hood
Processing
The script and each function is fully documented therefore as always ‘get-help’ will show your way around. The begin block in the script itself loads all the functions/scriptblocks/workflows, checks for the 64-bit workflow environment and for the Azure module availability. The process block executes the main routine which invokes the functions needed for the VM and Storage Account creation. For demo purposes the routine explicitly asks which credential, deployment image and which datacenters/regions to use instead of specifying these as parameter values to the script itself. After validating and processing the information PS workflow is used to create the storage accounts and VM’s in parallel. Finally the end block validates and confirms the creation and outputs the management functions and their related examples to use. PowerShell workflow in version 3 is limited with a hardcoded maximum of five simultaneous threads, there are some workarounds and solutions (runspaces/jobs) to expand the number of threads but it’s wiser to be on the safe side and avoid running into any kind of restriction or thread limitation in Azure or Azure module.
Management
When everything is up and running the ‘Execute-RemoteCommand’ facilitates in the commands and uses the ‘Setup-Session’ function to establish a connection to the VM and then executes a remote command against one or more VM’s. And last but not least the ‘ Generate-RDPFile’ generates a RDP file from the endpoint information and launches the RDP connection without any additional connection or authentication prompts. Additional VM’s in a particular region can be created with the ‘Create-AzureVM’ function. Everything is made super easy to use without prompts or any additional configuration. That said this script is made for a demo environment, in a production environment you have to review the code and see if it fits your needs, also from a security perspective point of view.
Functions and Code
Please use ‘Get-Help ‘function/script’ with the ‘-examples’ or ‘–full’ parameter to see what it exactly does and how you can invoke them. Below are all the (cmdlet based) functions you can use after you executed the script.
Create-AzureVM : Creates an VM in an Azure location determined by the specified storage account.
Create-AzureStorageAccount : Creates an Storage Account in a given Azure data center location.
ConnectTo-Azure : Connects to the Azure subscription.
CleanUp-Demo : Deletes all VM’s and tied storage accounts created with the demo.
Retrieve-Image: Retrieves the latest Windows Image from Azure, this image is used to deploy the VM with.
Index-VMinfo : Retrieves and indexes (caches) storage account and VM information from Azure.
Setup-Session: Sets up a remote PSsession to the AzureVM.
Execute-RemoteCommand : Allows you to execute remote PS commands to the Azure VM.
Update-PublicEndpointPort: Updates the Azure Public Endpoint port for the given VM’s, the endpoint port increments with one for each additional VM.
Generate-RDPFile : Generates a RDP connection file.
Delete-AzureVM : Deletes one or more VM’s.
Delete-AzureStorageAccount : Deletes one or more storage accounts.
Select-VM : Select one or more script indexed Azure VM’s.
I recommend you to download the script from TechNet for the latest version but if you want to see the code then I happily invite you to click on the bar below expanding the Crayon code window and browse through almost 2K lines of readable refactored code. Don’t forget to read the feedback section before expanding J
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 |
#Requires -Version 3 <# .NOTES File Name : AzureDemo-ProvisionVMsGlobally.ps1 Last Updated : May 26, 2015 Version : 0.91 Author : Ruud Borst ruud@ruudborst.nl Reviewer : Darryl van der Peijl darrylvanderpeijl@outlook.com Requires : PowerShell V3, Azure PowerShell Module, Azure Subscription .LINK http://www.ruudborst.nl .SYNOPSIS Rapid deploys an Azure Virtual Machine in every available Microsoft Azure Datacenter/Region. .DESCRIPTION This script helps you to automatically deploy an Azure VM, Azure Service and related Storage Account in each available Microsoft Azure Datacenter. It also facilitates in the provisioning and management of these VM's through the script advanced functions which are made available in the 'Global' scope and thus can be used in the console. This includes the VM and storage account provisioning, remote powerShell session setup, powershell remote command execution, rdp connection and public endpoint functions. This way it's fairly easy to provision the VM's in every Azure region available, use the resources (network/compute/storage) at that location and execute any remote ps commands to that VM. The 'Execute-RemoteCommand' facilitates in the session setup and let's you execute a command against one or more VM's and orders all results accordingly. All script parameters are optional and will be determined automatically when left blank. In addition to the configurable script parameters, the script's main routine will prompt for the administrator credentials, the Windows image file (deployment) and in which region/datacenter the VM's should be deployed. For demo purposes this script requires you to supply all the information instead of supplying the information to the script's parameters. When all information is validated PowerShell's parallel workflow execution will be used to deploy the VM's and related storage accounts. Note: PowerShell workflow in version 3 is limited with a hardcoded maximum of five simultaneous threads, there are some workarounds and solutions (runspaces/jobs) to expand the number of threads but it's wiser to be on the safe side and avoid running into any kind of restriction or thread limitation in Azure. Important: Please be aware of the Azure VM Core and Cloud Service limit in Azure (default 20), open up a support ticket with Microsoft to raise the limit. See: http://azure.microsoft.com/en-us/documentation/articles/azure-subscription-service-limits/#subscription-limits .PARAMETER SubscriptionName Supply the 'Subscriptionname' parameter when you want to use a particular subscription, by default it chooses the currently active one. .PARAMETER InstanceSize Used for the VM compute resource size (CPU,Memory,Disk), for example 'ExtraSmall','Small','Medium' etc. The default value is set to 'Small'. For more information see 'http://msdn.microsoft.com/library/azure/dn197896.aspx'. .PARAMETER VMprefix This prefix combined with the short name of the location generated by the script forms the unique VM and DNS name. The DNS name must be unique in Azure's public cloud DNS namespace, *.cloudapp.net. For example the name of the VM in 'East Asia' with prefix 'DEMO3-' forms 'DEMO3-EA'. .PARAMETER StorageAccountPrefix This prefix combined with the short name of the location generated by the script forms the unique Storage Account name in Azure. Storage account names use numbers and lower-case letters only. For example the name of the Storage Account in 'East Asia' with prefix 'demo3' forms 'demo3ea'. .PARAMETER ImportPubFile Specify this switch parameter to use the certificate import with 'Import-AzurePublishSettingsFile' instead of the default 'add-azureaccount' method where the account will expire after 12 hours. Simply said, by using the certificate method you don't have to re-authenticate every 12 hour. .PARAMETER AddSubscription Specify this switch parameter add a new Azure Subscription, only use this switch when you already have a existing Azure subscription and want to add a new one to use with this script. .EXAMPLE .\AzureDemo_ProvisionVMsGlobally.ps1 .EXAMPLE .\AzureDemo_ProvisionVMsGlobally.ps1 -SubscriptionName 'Enterprise' .EXAMPLE .\AzureDemo_ProvisionVMsGlobally.ps1 -AddSubscription .EXAMPLE .\AzureDemo_ProvisionVMsGlobally.ps1 -SubscriptionName 'Enterprise' -InstanceSize 'Small' -VMprefix 'DEMO3-' -StorageAccountPrefix 'demo3' -ImportPubFile -AddSubscription #> [CmdletBinding()] Param( [Parameter(HelpMessage = 'Enter the Azure subscription name, for example; Enterprise or Trial. Leave empty to determine the subscription automatically.',ParameterSetName = "2")] [Alias('Subscription','AzureSubscription')] [ValidateLength(3,50)] [String]$SubscriptionName, [Parameter(HelpMessage="Enter the casesensitive VM size, for example 'ExtraSmall','Small','Medium' etc. See 'http://msdn.microsoft.com/library/azure/dn197896.aspx' for available sizes.")] [String]$InstanceSize = 'Small', [Parameter(HelpMessage="This prefix combined with the the location forms the VM name, for example prefix 'VM-' will form the name 'VM-EA' in East Asia")] [ValidateLength(2,8)] [String]$VMprefix, [Parameter(HelpMessage="This prefix combined with the the location forms the storage account name, for example prefix 'stor' will form the name 'storeastasia' in East Asia")] [ValidateLength(2,8)] [String]$StorageAccountPrefix, [Parameter(HelpMessage="Specify this switch to use the certificate import instead of the 'Add-AzureAccount' method which expires after 12 hours.")] [Switch]$ImportPubFile, [Parameter(HelpMessage="Specify this switch to add a new Azure subscription")] [Switch]$AddSubscription ) BEGIN { # # Prerequisites check ## $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop # Checks for a 64bit environment, otherwise PS workflow won't work. if (![Environment]::Is64BitProcess){ write-warning 'This script uses Windows PowerShell Workflow which is not supported in a Windows PowerShell x86-based console. Open a Windows PowerShell x64-based console, and then try again.' break } # end if # Checks for the Azure module availability and import when necessary if (!(Get-Module -Name Azure -ListAvailable)) { $AzureModule = "${env:ProgramFiles(x86)}\Microsoft SDKs\Azure\PowerShell\ServiceManagement\Azure\Azure.psd1" if (Test-Path $AzureModule){ Import-Module $AzureModule -Global } else { Write-Warning 'Azure PowerShell module not detected, please download and install from ''https://github.com/Azure/azure-powershell/releases'' and rerun this script.' break } # end if azuremodule } else { Import-Module Azure } # end if Azuremodule check # # Functions ## Function Global:Create-AzureVM { <# .SYNOPSIS Creates an Azure VM in a Azure data center location determined by the specified storage account. .DESCRIPTION This function creates a Azure Virtual machine using the 'New-AzureQuickVM' cmdlet in an Azure data center location determined by the specified storage account. It takes the VM name (mandatory), instance size, image, storage account name (also represents the location), credential and subscription as input. The optional parameters default to the chosen values of the main script routine itself. Except for the 'StorageAccountName', when the 'StorageAccountName' is not specified the function will present a list with available storage accounts to choose from. You will be asked for the optional parameters when the default script variables are not populated. See the parameter descriptions for more information or use 'help Create-AzureVM -examples' to see several input examples. .PARAMETER Name One or more Virtual Machines to operate against. Accepts pipeline input ByValue and ByPropertyName. .PARAMETER InstanceSize Used for the VM compute resource size (CPU,Memory,Disk), for example 'ExtraSmall','Small','Medium' etc. Default value is set to 'Small'. For more information see 'http://msdn.microsoft.com/library/azure/dn197896.aspx' .PARAMETER ImageName Enter a available Azure VM image, like 'a699494373c04fc0bc8f2bb1389d6106__Windows-Server-2012-R2-201411.01-en.us-127GB.vhd'. When left blank, the function uses the 'ImageName' variable which was set by the script's main routine. If the variable is not set it executes the 'Retrieve-Image' function, this allows you to use the latest image for the selected Windows image family. .PARAMETER StorageAccountName This is the fully written Storage Account Name, all in lowercase and limited to 24 characters. .PARAMETER Credential Expects a PS credential object 'Get-Credential'. When left blank, it uses the 'VMcred' credential variable which was asked in the main routine, it requests a new credentials when the VMcred variable is empty. .PARAMETER SubscriptionName Supply the 'Subscriptionname' parameter when you want the script to use a particular subscription, by default it chooses the currently active one. .PARAMETER SleepinMS Used for the main routine only, built-in sleep, this we way PS workflow doesn't run into the dreadful AzureProfile.json being used by another process when switching the storage account in the active subscription. This change is written in the AzureProfile.json profile file which can't be accessed by multiple processes/threads simultaneously. .EXAMPLE Create-AzureVM VM1 .EXAMPLE Create-AzureVM VM2 -Instance Large -ImageName (Retrieve-Image) -Credential (Get-Credential) .EXAMPLE Create-AzureVM VM3 -Instance Small -ImageName 'a699494373c04fc0bc8f2bb1389d6106__Windows-Server-2012-R2-201411.01-en.us-127GB.vhd' -StorageAccountName storeastasia -Credential $VMCred .LINK www.ruudborst.nl #> [CmdletBinding(SupportsShouldProcess = $True)] Param( [Parameter(Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True,HelpMessage='Enter the virtual machine name')] [Alias('ComputerName','MachineName','Server','VM')] [ValidateLength(3,15)] [string[]]$Name, [Parameter(HelpMessage = "Enter the casesensitive VM size, for example 'ExtraSmall','Small','Medium' etc. See 'http://msdn.microsoft.com/library/azure/dn197896.aspx'.")] [Alias('Instance')] [String]$InstanceSize = $InstanceSize, [Parameter(HelpMessage = "Supply a available Azure VM image, like ` 'a699494373c04fc0bc8f2bb1389d6106__Windows-Server-2012-R2-201411.01-en.us-127GB.vhd'")] [ValidateScript({try {if (!(get-AzureVMimage -imagename $_ -ea 0)){$false}else{$True}}catch{$true}})] [String]$ImageName = $ImageName, [Parameter(HelpMessage = 'Enter the Storage Account name')] [Alias('StorageAccount')] [ValidateLength(3,24)] [ValidateScript({$_ -cmatch "^[a-z0-9]*$"})] [string]$StorageAccountName = ((Get-AzureStorageAccount -Wa 0).StorageAccountName ` | Out-GridView -Title 'Please select a Storage Account' -PassThru), [Parameter(HelpMessage = 'Supply a PSCredential object (username/password)')] [Alias('VMcred')] [PSCredential]$Credential = (Get-Credential $VMcred), [Parameter(HelpMessage = 'Enter the Azure subscription name, for example; Enterprise or Trial. Leave empty to determine the subscription automatically.')] [Alias('Subscription','AzureSubscription')] [ValidateLength(3,50)] [String]$SubscriptionName, [Parameter(HelpMessage = 'For PS workflow multiple processes usage.')] [int]$SleepinMS ) BEGIN { # Creates a hashtable to store the newly created VM's. # This way we won't have to execute a slow live query to Azure. if (!$VMtoLoc) { $Global:VMtoLoc = @{} } # end if vmtoloc # Checks for Azure module, needed for workflow execution when the module is just installed, this way you won't have to restart the PowerShell console if (!(Get-Module -Name Azure -ListAvailable)) { $AzureModule = 'C:\Program Files (x86)\Microsoft SDKs\Azure\PowerShell\ServiceManagement\Azure\Azure.psd1' if (Test-Path $AzureModule){ Import-Module $AzureModule -Global } else{ Write-Warning 'Azure PowerShell module not detected, please download and install from ''https://github.com/Azure/azure-powershell/releases'' and rerun this script.' break } # end if azuremodule } else { Import-Module Azure } # end if Azuremodule check # Allows you to select the image via the 'Retrieve-Image' function when the ImageName parameter is empty if (!$ImageName) { Write-Host "Could not determine the image file name, please choose which image you want to use, executing function 'Retrieve-Image' ..." $Global:ImageName = Retrieve-Image } # end if image # Built-in sleep, this we way PS workflow doesn't run into the dreadful AzureProfile.json being used by another process # when you switch the storage account in the active subscription, the change happens in the profile file which can't be accessed by multiple processes/threads at once if ($SleepinMS){ Sleep -Milliseconds $SleepinMS } # end if SleepinMS # Sets the subscriptionname to the current active subscription name when not supplied if (!$SubscriptionName){ $SubscriptionName = (Get-AzureSubscription | ? IsCurrent -eq $True).SubscriptionName } # end if !SubscriptionName # Sets the new supplied credential as the globally used default credential if (!$VMcred){ $Global:VMcred = $Credential } # end if # Retrieve the storage account location $Location = (Get-AzureStorageAccount $StorageAccountName -Wa 0).Location # Sets the subscription to work with the selected storage account Set-AzureSubscription -currentstorageaccount $StorageAccountName -SubscriptionName $SubscriptionName } # end BEGIN block PROCESS { if ($pscmdlet.ShouldProcess($Name)) { $Name | ForEach -Process { $VM = $_ # Creates the VM when it doesn't exist if (!(Get-AzureVM $VM -Wa 0 -Ea 0)) { "[Creating] '$VM' in '$Location' ..." try { New-AzureQuickVM -Windows -ServiceName $VM -name $VM -ImageName $ImageName -Password ($Credential.GetNetworkCredential().Password) ` -InstanceSize $InstanceSize -adminuser $Credential.UserName -Location $Location ` -MediaLocation "http://$StorageAccountName.blob.core.windows.net/vhds/$VM.vhd" -ErrorAction Stop | Out-Null $VMtoLoc[$VM] = $Location "[Created] '$VM' in '$Location'" } catch { "[Failed] '$VM' in '$Location'" write-error $_.Exception.Message } # end try/catch } else { "[Exists] '$VM' already exists." } # end if Get-AzureVM } # end foreach Name } # end if ShouldProcess } # end process block } # end Create-AzureVM function Function Global:Create-AzureStorageAccount { <# .SYNOPSIS Creates an Azure Storage Account based in given Azure data center location. .DESCRIPTION This function creates a storage account using the 'New-AzureStorageAccount' cmdlet in the specified datacenter location. .PARAMETER Name The fully written Storage Account Name, all in lowercase and limited to 24 characters. Accepts pipeline input ByValue and ByPropertyName. .PARAMETER Location The Azure datacenter location name, use the 'Get-AzureLocation' cmdlet for a list of available datacenters. .EXAMPLE Create-AzureStorageAccount stornorthcentralus -Location 'North Central US' .LINK www.ruudborst.nl #> [CmdletBinding(SupportsShouldProcess = $True)] Param( [Parameter(Mandatory = $True,ValueFromPipeline = $True,ValueFromPipelineByPropertyName=$True,HelpMessage = 'Enter the Storage Account name')] [Alias('StorageAccount','StorageAccountName')] [ValidateLength(3,24)] [ValidateScript({$_ -cmatch "^[a-z0-9]*$"})] [string[]]$Name, [Parameter()] [ValidateLength(3,24)] [string]$Location = ((Get-AzureLocation | where availableservices -Contains 'Compute').name | Out-GridView -PassThru -Title "Select one data center location to create the storage account in.") ) BEGIN { # Creates a hashtable to store the newly created accounts. # This way we won't have to execute a slow live query to Azure. if (!$STtoLoc) { $Global:STtoLoc = @{} } # end if Stloc # Checks for Azure module, needed for workflow execution when the module is just installed, this way you won't have to restart the PowerShell console if (!(Get-Module -Name Azure -ListAvailable)) { $AzureModule = 'C:\Program Files (x86)\Microsoft SDKs\Azure\PowerShell\ServiceManagement\Azure\Azure.psd1' if (Test-Path $AzureModule){ Import-Module $AzureModule -Global } else{ Write-Warning 'Azure PowerShell module not detected, please download and install from ''https://github.com/Azure/azure-powershell/releases'' and rerun this script.' break } # end if azuremodule } else { Import-Module Azure } # end if Azuremodule check } # end BEGIN block PROCESS { if ($pscmdlet.ShouldProcess($Name)) { $Name | ForEach -Process { try { # Creates the Storage Account in the given datacenter location if (!(Get-AzureStorageAccount $_ -Wa 0 -Ea 0)) { "[Creating] '$_' in '$Location' ..." New-AzureStorageAccount -StorageAccountName $_ -location $Location | Out-Null "[Created] '$_' in '$Location'" $STtoLoc[$_] = $Location } else { "[Exists] '$_' in '$Location'" } # end if Storage Account created } Catch { Write-Host "An error occured trying to create the Storage Account '$Name' in location '$Location'" write-error $_.Exception.Message } # end try/catch } # end foreach Name } # end if ShouldProcess } # end process block } # end Create-AzureStorageAccount function Function Global:ConnectTo-Azure { <# .SYNOPSIS Connects to the Azure subscription. .DESCRIPTION This function tries retrieves the subscription details and establishes an administrative connection to Azure. .EXAMPLE ConnectTo-Azure .PARAMETER SubscriptionName Enter the Azure subscription name, leave blank to determine the name automatically. .PARAMETER ImportPubFile Specify this switch parameter to use the certificate import instead of the default 'add-azureaccount' method which expires after 12 hours. By using the certificate method you don't have to authenticate each time your 12 hour session expires. .PARAMETER AddSubscription Specify this switch parameter add a new Azure Subscription via the 'Add-AzureAccount method, only use this switch when you already have a existing Azure subscription and want to add a new one to use with this script. #> [CmdletBinding()] Param( [Parameter(HelpMessage = 'Enter the Azure subscription name')] [Alias('Subscription','AzureSubscription')] [ValidateLength(3,50)] [String]$SubscriptionName = $SubscriptionName, [Parameter(HelpMessage="Specify this switch to use the certificate import instead of the 'Add-AzureAccount' method which expires after 12 hours.")] [Switch]$ImportPubFile, [Parameter(HelpMessage="Specify this switch to add a new Azure subscription.")] [Switch]$AddSubscription ) Write-Host "`n - [$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan # Delete expired azure user accounts when the import publish certificate file is used. # Otherwise the subscription tries to use the already used but expired azure user account. if ($ImportPubFile){ Get-AzureAccount | where type -eq user | foreach { Remove-AzureAccount -name $_.id -force -WarningAction SilentlyContinue -ErrorAction SilentlyContinue } # end foreach get-azureaccount } # end if ImportPubFile function ImportPubFile { "`nUsing the import publish file method to retrieve the subscription." "For more information see 'http://msdn.microsoft.com/en-us/library/dn495124.aspx'" Sleep 2 "The default browser will redirect you to the Azure portal where the subscription file can be retrieved. Save this file on the filesystem and specify the directory in the next prompt. The file will be deleted afterwards." Pause Get-AzurePublishSettingsFile $PublishSettingsFileDir = Read-Host -Prompt 'Please specify the directory containing the saved connection file' $FileDirExists = Test-Path $PublishSettingsFileDir if ($FileDirExists -eq $True) { $PublishSettingsFile = Get-ChildItem $PublishSettingsFileDir -Filter *.publishsettings | Sort lastwritetime | Select -Last 1 if ($PublishSettingsFile) { Write-Host "`nImporting publish file $PublishSettingsFile ..." Sleep -Seconds 2 Import-AzurePublishSettingsFile $PublishSettingsFile.FullName -Verbose "Deleting import publish file $PublishSettingsFile ..." Remove-Item -Path $PublishSettingsFile.FullName -Force -Verbose } else { Write-Warning -Message "Couldn't locate the file in directory $PublishSettingsFileDir." 'Please try again or execute the steps manually via`nhttp://azure.microsoft.com/en-us/documentation/articles/install-configure-powershell/ and re-run the script.' } #end if PublishSettingsFile } else { Write-Error -Message "Directory $PublishSettingsFileDir doesn't exist. Aborting script." break } # end if FileDirExists } # end importpubfile function if ($AddSubscription){ write-host "'AddSubscription' parameter specified, adding a new subscription ..." if (!$ImportPubFile){ Add-AzureAccount }else{ . ImportPubFile } # end if importpubfile } # end if AddSubscription $AzureSubscription = Get-AzureSubscription -ea 0 $SubscriptionCount = ($AzureSubscription).count if ($AzureSubscription -and $SubscriptionName){ $Subscription = $AzureSubscription | Where SubscriptionName -eq $SubscriptionName if (!$Subscription){ Write-Warning -Message "No registered Azure Subscription found with subscription name $SubscriptionName" } # end if Subscription } elseif ($SubscriptionCount -eq 1 -and !$SubscriptionName){ write-host "`nSubscriptionName parameter is not specified, trying to use the current active subscription.`nIf you don't see your subscription then please run the script or this function with parameter '-AddSubscription'." $Subscription = $AzureSubscription | Where IsCurrent -eq $True } elseif ($SubscriptionCount-gt 1 -and !$SubscriptionName){ write-host "`nSubscriptionName parameter is not specified, found multiple subscriptions, please choose a subscription to use.`nIf you don't see your subscription then please run the script or this function with parameter '-AddSubscription'." Pause $Subscription = $AzureSubscription | where SubscriptionName -eq ($AzureSubscription | Select SubscriptionName,Accounts,IsDefault,IsCurrent | Out-Gridview -Title 'Please select the subcription to use' -PassThru).SubscriptionName } # end if AzureSubscription/SubscriptionName if (!$Subscription){ "No registered or active Azure Subscription found, trying to retrieve the subscription details from Azure ..." Pause # Execute the importpubfile function in this function when the importpubfile parameter is specified otherwise the add-azureaccount Azure cmdlet is used if (!$ImportPubFile){ Add-AzureAccount }else{ . ImportPubFile } # end if importpubfile $AzureSubscription = Get-AzureSubscription -ea 0 if ($AzureSubscription -and $SubscriptionName){ $Subscription = $AzureSubscription | Where SubscriptionName -eq $SubscriptionName if (!$Subscription){ Write-Warning -Message "No registered Azure Subscription found with subscription name $SubscriptionName. Please re-run the function/script with a valid subscriptionname or leave it empty to use the current active one." } # end if Subscription } elseif ($AzureSubscription -and !$SubscriptionName){ Write-Host "`nSubscriptionName parameter is not specified, trying to use the current active subscription." $Subscription = $AzureSubscription | Where IsCurrent -eq $True if (!$Subscription){ Write-Warning -Message "No registered or active Azure Subscription found. 'Please try again or use 'ConnectTo-Azure' with the '-ImportPubFile' parameter to use the Import publish file settings method. You could also execute the steps manually via`n'http://azure.microsoft.com/en-us/documentation/articles/install-configure-powershell/' and re-run the script." break } # end if Subscription } # end if AzureSubscription } # end if !Subscription $SubscriptionName = $Subscription.SubscriptionName Select-AzureSubscription -SubscriptionName $SubscriptionName Write-Host "`nSelected the '$SubscriptionName' subscription as the active Azure subscription.`n" $Global:SubscriptionName = $SubscriptionName } # end function ConnectTo-Azure Function Global:CleanUp-Demo { <# .SYNOPSIS Deletes all VM's and tied storage accounts created with this script. .DESCRIPTION This function triggers the 'Delete-AzureVM' and the 'Delete-AzureStorageAccount' function and helps you to clean-up the demo VM's and related storage accounts. .LINK www.ruudborst.nl #> Write-Host "`n - [$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan # Identify the VM's and Storage Accounts created with last execution of this script if ($Vmprefix -and $Storageaccountprefix){ $DemoVMs = $VMtoLoc.keys | where {$_ -match "^$Vmprefix"} $DemoSTs = $STtoLoc.keys | where {$_ -match "^$Storageaccountprefix"} 'This function triggers the ''Delete-AzureVM'' and the ''Delete-AzureStorageAccount'' function and helps you to clean-up the demo VM''s and related storage accounts created with this script.' Pause if ($DemoVMs -ge 1){ "`nDemo VM's" '-------------' $DemoVMs # Delete the Azure VM through the script's 'Delete-AzureVM' function , a confirmation will be asked for each VM. $DemoVMs | Delete-AzureVM } else { "`nNo VM's found with prefix $VMprefix. Please try 'Delete-AzureVM -Retrieve' and choose one or more VM's.`n" } # end if DemoVMs if ($DemoSTs -ge 1){ Write-Host "`n - [$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan "`nDemo Storage Accounts" '-------------' $DemoSTs # Delete the Azure Storage Account through the script's 'Delete-AzureStorageAccount' function , a confirmation will be asked for each Storage Account. $DemoSTs | sort | Delete-AzureStorageAccount } else { "No storage accounts found with prefix $Storageaccountprefix. Please try 'Delete-AzureStorageAccount -Retrieve' and choose the storage account you want to delete." } # end if DemoSTs } else { Write-Warning "Prefix isn't set, could not identify demo Azure objects. Please try to delete the objects manually via 'Delete-AzureVM -Retrieve' or 'Delete-AzureStorageAccount -Retrieve'" } # end if prefix } # end CleanUp-Demo function Function Global:Retrieve-Image { <# .SYNOPSIS Retrieves the latest Windows Image from Azure, this image is used for the VM deployment. .DESCRIPTION This function retrieves all Windows family type images with the 'Get-AzureVMImage' cmdlet and prompts which family type you want to use. It then selects the latest published image for the chosen family type and sets that name as a string to the imagename script variable. .EXAMPLE Retrieve-Image #> # Retrieve all Windows Family Images $Images = Get-AzureVMImage | Where { $_.imagefamily -match '^Windows Server'} # Prompt which image family to use and select the latest image for it $ImageFamily = ($Images | Group imagefamily).name | Out-GridView -Title 'Choose a Windows OS, the script will automatically select the latest image.' -PassThru if ($ImageFamily) { $Global:ImageName = ($Images | Where{ $_.imagefamily -eq $ImageFamily } | Sort publisheddate -Descending | Select -First 1).ImageName $ImageName } else { 'No input received.' } # end if ImageFamily } # end retrieve-image function Function Global:Index-VMinfo { <# .SYNOPSIS Retrieves and indexes storage account and VM information from Azure. .DESCRIPTION Retrieves VM and storage account information from Azure and stores it into the relevant hash tables. The function uses the script's VM and storage account prefix to retrieve only the created objects created with this script. Specify the 'All' parameter to index all VM's and storage accounts. Information from this function is retrieved by the 'Select-VM' function which allows you to select a VM without waiting for any slow running Azure queries. .PARAMETER All Specify the All switch parameter to index all VM's under the current Azure subscription and not just the ones created by this script. .PARAMETER PreVM Specify the VM prefix. .PARAMETER PreSt Specify the storage account prefix. .EXAMPLE Index-VMinfo .EXAMPLE Index-VMinfo -All .EXAMPLE Index-VMinfo -PreVM demo9 -PreSt demo9 #> [CmdletBinding(DefaultParametersetName="2")] Param( [Parameter(ParameterSetName = "1",HelpMessage="This prefix combined with the the location forms the VM name, for example prefix 'VM-' will form the name 'VM-EA' in East Asia")] [String]$PreVM=$VMprefix, [Parameter(ParameterSetName = "1",HelpMessage="This prefix combined with the the location forms the storage account name, for example prefix 'stor' will form the name 'storeastasia' in East Asia")] [String]$PreSt=$StorageAccountPrefix, [Parameter(ParameterSetName = "2")] [Switch]$All, [Parameter(ParameterSetName = "1")] [Switch]$NoInfo ) # Make sure prefixes are null when specifying the '-All' parameter switch if ($All){ $PreVM = $null $PreSt = $null } # end if All Write-Host "`n - [$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan Write-Host 'Retrieving and indexing VM and storage information.' # Defining all global indexing variables if they don't already exist if (!$VMtoLoc){$Global:VMtoLoc = @{}} if (!$VMtoUri){$Global:VMtoUri = @{}} if (!$VMtoRDP){$Global:VMtoRDP = @{}} if (!$VMtoIP){$Global:VMtoIP = @{}} if (!$WinRMFwInfo){$Global:WinRMFwInfo = @()} if (!$STtoLoc){$Global:STtoLoc = @{}} Write-Host "`nRetrieving Storage Accounts using 'Get-AzureStorageAccount' ..." $GetAzureST = Get-AzureStorageAccount -wa 0 | Where StorageAccountName -match "^$PreSt" $GetAzureST | ForEach { $STtoLoc[$_.StorageAccountName] = $_.Location } # end foreach GetAzureST # Outputs retrieved and indexed storage account information when the NoInfo switch isn't specified if (!$NoInfo){ $GetAzureST | ft StorageAccountName,Location,@{Name="Endpoint";Expression={$_.endpoints[0]}} -AutoSize } # end if NoInfo Write-Host "`nRetrieving VM's and Endpoints using 'Get-AzureVM' ..." $GetAzureVM = Get-AzureVM | Where Name -match "^$PreVM" # Indexes and converts all VM information including RemoteDesktop and PowerShell url's into hash tables if ($GetAzureVM.count -ge 1){ $GetAzureVM | ForEach -Process { $St = $_.vm.OSVirtualHardDisk.MediaLink.Host -replace "\.blob.core.windows.net","" $VMname = $_.Name $VMIP = $_.IpAddress $Endpoint = $_ | Get-AzureEndpoint | where name -eq 'PowerShell' $IP = $Endpoint.vip $Port = $Endpoint.port $Uri = "https://$IP`:$Port" $Global:VMtoUri[$VMname] = $Uri $Global:VMtoIP[$VMname] = $VMIP $Endpoint = $_ | Get-AzureEndpoint | where name -eq 'RemoteDesktop' if ($_.DNSName -match "//(.*)/"){$hostname = $Matches[1]} $RDPaddress = $hostname + ":" + $Endpoint.Port $Global:VMtoRDP[$VMname] = $RDPaddress $VMtoLoc[$VMname] = $STtoLoc[$St] $WinRMFwInfo += [pscustomobject]@{ VM = $VMname Size = $_.InstanceSize Location = $VMtoLoc[$VMname] URI = $_.DNSName PSport = $Port } # end pscustomobject } # end foreach GetAzureVM } else { 'No VM''s found.' } # end if GetAzureVM.count # Outputs VM name,size,location and remote PS information when the NoInfo switch isn't specified if (!$NoInfo){ $WinRMFwInfo | ft -AutoSize } # end if NoInfo } # end function Index-VMinfo Function Global:Setup-Session { <# .SYNOPSIS Sets up a remote PSsession to the AzureVM. .DESCRIPTION Checks for an existing PSsession for the supplied VM and if it is not open yet opens up a new one with the 'New-PSSession' cmdlet. The PSsession is supplied with a SessionOption parameter which ignores the Certificate Authority check and sets a idle timeout of one hour. This function is used by the 'execute-remotecommand' function to set up the VM connection before executing a remote PS command. .PARAMETER Name One or more Virtual Machines to operate against. Accepts pipeline input ByValue and ByPropertyName. .PARAMETER Credential By default the script uses the VMcred credential which was used to create the VM's by this script. You can specify a different PScredential object or execute '(get-credential)'. .EXAMPLE Setup-Session VM-NCU .EXAMPLE Select-VM -All | Setup-Session -Cred (get-credential) #> [CmdletBinding(SupportsShouldProcess = $True)] Param( [Parameter(Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True,HelpMessage='Enter the VM name')] [Alias('ComputerName','MachineName','Server','VM')] [ValidateLength(3,15)] [string[]]$Name, [Parameter(HelpMessage = "Specify a different PScredential object or execute '(get-credential)'. By default it uses the credentials which was used for the VM creation.")] [Alias('cred')] [PsCredential]$Credential ) BEGIN { # Skip CA PSsession check and configure a 1hour idletimeout if (!$SessionOption) { $Global:SessionOption = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck -IdleTimeout 3600000 } # end if if (!$VMtoUri){$Global:VMtoUri = @{}} if (!$VMtoIP){$Global:VMtoIP = @{}} if (!$WinRMFwInfo){$Global:WinRMFwInfo = @()} } # end begin block PROCESS { if ($pscmdlet.ShouldProcess($Name)) { $Name | foreach { $VM = $_ [string]$state = (Get-PSSession -Name $VM -ea 0).state if ($state -ne 'Opened' -or $Credential) { Write-Host "`n - [$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan if (!$VMtoUri[$VM]) { "Could not find '$VM' WinRM remote management url in the VMtoUri hashtable.`n" "Press 'y' to execute the 'Index-VMinfo -All' function and index all VM WinRM url's from Azure or press any other key to abort the session setup.`n" $IndexVMs = Read-Host -Prompt "Index WinRM url's (y/n)?" if ($IndexVMs -eq 'y') { Index-VMinfo -All Write-Host "`n - [$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan } else { break } # end if indexvms } # end if vmtouri Write-Host "Establishing a remote session to $VM ..." if (!$VMcred -and !$Credential) { $Global:VMcred = Get-Credential -Message "Please supply the Azure Virtual Machine Windows Administrator`n user name and password." } # end if vmcred/!credential Remove-PSSession -Name $VM -ErrorAction SilentlyContinue -WarningAction SilentlyContinue if ($Credential){ $SesCred=$Credential }else{ $SesCred=$Vmcred } # end if Credential try { $WinRMuri = $VMtoUri[$VM] New-PSSession -Credential $SesCred -ConnectionUri $WinRMuri -SessionOption $SessionOption -Name $VM -erroraction stop;'' } catch { Write-Host "`n" write-warning "An error occured trying to establish a PSSession to $WinRMuri`nPlease verify connectivity to the '$VM' or wait for the VM to start up." Write-Host "`n" Write-Host $_.exception.message } # end catch } # end if state $IP = $VMtoIP[$VM] $Loc = $VMtoLoc[$VM] $Os = $VMtoOS[$VM] $Sessioninfo = [pscustomobject]@{ VM = "$VM" } if ($Loc) { $Sessioninfo = [pscustomobject]@{ VM = "$VM" IP = "$IP" Location = "$Loc" } } # end if if ($Os) { $Sessioninfo = [pscustomobject]@{ VM = "$VM" IP = "$IP" Location = "$Loc" OS = "$Os" } } # end if } # end if ShouldProcess } # end foreach VM } # end process block } # end function Setup-Session Function Global:Execute-RemoteCommand { <# .SYNOPSIS Allows you to execute remote PowerShell commands to the Azure VM. .DESCRIPTION Executes the supplied command provided in the scriptblock parameter with the 'invoke-command' cmdlet on one or more Azure VM's supplied in the Name variable to this function. Also a new session will be created by the 'setup-session' function if there isn't one already. .PARAMETER Name One or more Virtual Machines to operate against. Accepts pipeline input ByValue and ByPropertyName. .PARAMETER ScriptBlock Is mandatory and accepts a scriptblock '-Scriptblock {'hello world'}' as input. .PARAMETER Credential By default the script uses the VMcred credential which was used to create the VM's by this script. You can specify a different PScredential object or execute '-credential (get-credential)'. .EXAMPLE Select-VM -All | Execute-RemoteCommand .EXAMPLE Execute-RemoteCommand VM-NCU -Scriptblock $httpconnect .EXAMPLE Execute-RemoteCommand VM-NCU -Scriptblock {$psversiontable} -Credential (get-credential) .LINK www.ruudborst.nl #> [CmdletBinding(SupportsShouldProcess = $True)] Param( [Parameter(Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True,HelpMessage='Enter the VM name')] [Alias('ComputerName','MachineName','Server','VM')] [ValidateLength(3,15)] [string[]]$Name, [Parameter(HelpMessage = "Specify a PowerShell ScriptBlock or supply your own, for example '{whoami}'")] [Alias('sb')] [ScriptBlock]$ScriptBlock=$httpconnect, [Parameter(HelpMessage = "Specify a different PScredential object or execute '(get-credential)'. By default it uses the credentials which are used for the VM creation.")] [Alias('cred')] [PsCredential]$Credential ) BEGIN { Write-Host "`n - [$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan # Instantiate VM to OS hash if it doesn't exist to store the OS to VM mapping if (!$Global:VMtoOS) { $Global:VMtoOS = @{} } # end if VMtoOS $Errmessages=@() $Global:Results = @() } # end begin block PROCESS { if ($pscmdlet.ShouldProcess($Name)) { $Name | ForEach { $Output = $null $OSinfo = $null $VMe = $_.toUpper() # Initiates and checks for a WinRM Remote PS session Setup-Session $VMe $Credential $Session = Get-PSSession -Name $VMe -ErrorAction 0 if ($Session){ # Store the OS information in the hashtable if it doesn't exist if (!$VMtoOS[$VMe]) { $OSinfo = Invoke-Command -Session (Get-PSSession -Name $VMe) -ScriptBlock { (Get-WmiObject -Class win32_operatingsystem).caption } # end scriptblock $Global:VMtoOS[$VMe] = $OSinfo } # end if VMtoOS # Execute the actual scriptblock command $Output = Invoke-Command -Session (Get-PSSession -Name $VMe) -ScriptBlock $ScriptBlock Write-Host "[$VMe]" -ForegroundColor DarkCyan -NoNewline if (!$Output){$Output = ''} # Add the location and OS when the output is a PSCustomObject and add it to the results array if ($Output.gettype().name -eq 'PSCustomObject') { $Output.PSObject.Properties.Remove('Runspaceid') $Output.PSObject.Properties.Remove('pscomputername') $Output | Format-List $Output | Add-Member -MemberType noteproperty -Name 'Location' -Value $VMtoLoc[$VMe] $Output | Add-Member -MemberType noteproperty -Name 'OS' -Value $VMtoOS[$VMe] $Results += $Output } else { $Result = [pscustomobject]@{ VM = $VMe Result = $Output Location = $VMtoLoc[$VMe] OS = $VMtoOS[$VMe] } $Results += $Result $Result | Format-List } # end if Output } else { $Errmessages += "Could not locate PSsession for '$VME', please verify connectivity." } # end if } # end foreach } # end if shouldprocess } # end process block END { # Output and order results if ($Results) { Write-Host "`n - [$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan $Errmessages Write-Host "`n`tOrdered Results" -ForegroundColor Cyan $Results | Sort DurationinMS,result | select * -ExcludeProperty url, httpconnect | Format-Table -AutoSize # Set results to a global accessible variable for further analysis $Global:Results = $Results Write-Host 'Tip: You can use the $Results array variable to sort or expand the results.' } # end results } # end end block } # end function Execute-RemoteCommand Function Global:Update-PublicEndpointPort { <# .SYNOPSIS Updates the Azure Public Endpoint port for the given VM's, the endpoint port increments with one for each additional VM. .DESCRIPTION In case of outside FireWall restrictions prohibiting Azure's upper port ranges, it may be necessary to update the Public WinRM or Public RemoteDesktop TCP port.The EndPointPort parameter is incremented by one for each additional VM. This function accepts one or more VM's in an array in the Name parameter, the parameter EndPointName as a string and the EndPointPort parameter as an integer. .PARAMETER Name One or more Virtual Machines to operate against. Accepts pipeline input ByValue and ByPropertyName. .PARAMETER EndpointName Is mandatory and accepts RemoteDesktop or PowerShell as the endpoint name. .PARAMETER EndPointPort Is mandatory and accepts the TCP port range 1-65535 .EXAMPLE Select-VM | Update-PublicEndpointPort -EndPointName PowerShell -EndPointPort 5000 .EXAMPLE Update-PublicEndpointPort VM-EA RemoteDesktop 6000 .LINK www.ruudborst.nl #> [CmdletBinding(SupportsShouldProcess = $True)] Param( [Parameter(Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True,HelpMessage='Enter the VM name')] [Alias('ComputerName','MachineName','Server','VM')] [ValidateLength(3,15)] [string[]]$Name, [Parameter(HelpMessage = 'Choose RemoteDesktop or PowerShell')] [ValidateSet('RemoteDesktop', 'PowerShell')] [string]$EndpointName="PowerShell", [Parameter(HelpMessage = 'Valid port range 1-65535')] [ValidateRange(1,65535)] [int]$EndPointPort= (get-random -Minimum 6000 -Maximum 9000) ) BEGIN { Write-Host "`n - [$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan } # end begin block PROCESS { if ($pscmdlet.ShouldProcess($Name)) { $Name | ForEach -Process { Write-Host "`n[$_ | $($VMtoLoc[$_])]" -ForegroundColor DarkCyan Write-Host "`nUpdating $EndpointName public port to $EndPointPort ..." Try { Get-AzureVM $_ | Set-AzureEndpoint -Name $EndpointName -PublicPort $EndPointPort | Update-AzureVM if ($VMtoUri[$_] -and $EndpointName -eq "PowerShell"){$VMtoUri[$_] = $VMtoUri[$_] -replace ":[0-9]*$",":$EndPointPort"} if ($VMtoRDP[$_] -and $EndpointName -eq "RemoteDesktop"){$VMtoRDP[$_] = $VMtoRDP[$_] -replace ":[0-9]*$",":$EndPointPort"} $EndPointPort++ } catch { write-warning $_.Exception.Message "Failed updating endpoint $EndpointName, make sure the endpoint named '$EndpointName' exists. Use 'New-ExternalEndpoint' to define it." } # end catch } # end foreach vm } # end shouldprocess } # end process block } # end Update-PublicEndpointPort Function Global:Generate-RDPFile { <# .SYNOPSIS Generates a RDP connection file. .DESCRIPTION Generates the RDP file in the current user's temporary directory. Supply the Directory parameter to save the file in a directory other then the default temporary directory. Supply the NoLaunch switch parameter to skip launching the Remote Desktop connection (mstsc). Supply the NoPass switch parameter to save the RDP file without secure password. .PARAMETER Name One or more Virtual Machines to operate against. Accepts pipeline input ByValue and ByPropertyName. .PARAMETER Directory Supply the directory for the directory parameter where to save the RDP files in. The file won't be deleted afterwards. Leave empty to use the current user's temporary directory retrieved via '$env:TEMP'. .PARAMETER NoLaunch Supply the NoLaunch switch parameter without any value, this way the remote desktop file won't be launched with mstsc (Remote Desktop connection). .PARAMETER NoPass Prevents the password being saved into the RDP file as an secure string. By default the function retrieves the password from the VMcred PScredential object which was used to set the VM 'administrator' account. This way we are able to logon without any logon prompt. .EXAMPLE Generate-RDPFile vm-ea Generates a RDP file for VM 'VM-EA' in the $env:TEMP temporary directory and deletes the file afterwards. .EXAMPLE Select-VM | Generate-RDPFile -Directory c:\users\ruudborst\desktop -NoLaunch Allows you to select one or more newly created VM's indexed by the script, generate a RDP file in the chosen directory and disable the launch of the RDP connection. .LINK www.ruudborst.nl #> [CmdletBinding(SupportsShouldProcess = $True)] Param( [Parameter(Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True,HelpMessage='Enter the VM name')] [Alias('ComputerName','MachineName','Server','VM')] [ValidateLength(3,15)] [string[]]$Name, [Parameter(HelpMessage = 'Enter the RDP file destination path')] [ValidateScript({if ($_ -match '\\'){Test-Path $_}else{$True}})] [string]$Directory, [Parameter()] [switch]$NoLaunch, [Parameter()] [switch]$NoPass ) BEGIN { Write-Host "`n - [$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan } PROCESS { if ($pscmdlet.ShouldProcess($Name)) { $Name | ForEach -Process { $VMname = $_ if (!$VMtoRDP[$VMname]) { "Could not find '$VMname' RDP endpoint $vmtorurl in the VMtoRDP hashtable.`n" "Press 'y' to execute the 'Index-VMinfo -All' function and index all VM RDP and WinRM url's from Azure or press any other key to abort.`n" $IndexVMs = Read-Host -Prompt "Index RDP endpoint url's (y/n)?" if ($IndexVMs -eq 'y') { Index-VMinfo -All Write-Host "`n - [$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan if (!$VMtoRDP[$VMname]){ "$VMname not found in your Azure subscription." break } # end if VMtoRDP } else { break } # end if indexvms } # end if VMtoRDP # Save in specified directory, save in temporary directory and delete afterwards if directory is not specified if ($Directory) { $RDPfilepath = "$Directory\$VMname.rdp" } else { $RDPfilepath = "$env:TEMP\$VMname.rdp" } #end if directory Write-Host "`n[$_ | $($VMtoLoc[$VMname])]" -ForegroundColor DarkCyan "`nGenerating RDP File ...`n" # Sets the username from the VMcred credential and the RDP address retrieved via the 'index-vminfo' function # For Single PowerShell Function Execution Sign On (SPFESO) the authentication level is set to 'no authentication/Connect and don’t warn me'. $Username = $VMcred.UserName set-content $RDPfilepath -Value "full address:s:$($VMtoRDP[$VMname])" add-content $RDPfilepath -Value "username:s:$Username" add-content $RDPfilepath -Value "authentication level:i:0" # If the nopass parameter isn't supplied and VMcred credential is not empty # then save the password from the VMcred credential as an encrypted password in the RDP file if (!$NoPass -and $vmcred){ $Ss = $VMcred.password | ConvertFrom-SecureString add-content $RDPfilepath -Value "password 51:b:$Ss" if ($vmtordp[$vmname] -match "(.*):"){ $RDPfqdn = $matches[1] } # end if vmtordp # Ask for registry write confirmation and write a registry entry in HKCU for Single PowerShell Function Execution Sign On (SPFESO) :) if (!$WriteReg){ $Global:WriteReg = read-host -Prompt "In order to avoid the unknown RDP publisher warning and SSO to the Demo VM's it is necessary to set the following reg key for the RDP client: 'HKCU:\Software\Microsoft\Terminal Server Client\LocalDevices\VMname' to DWORD decimal value 76. This is the 'don't ask me again' tick box setting when you launch a RDP connection. Make it SSO (y/n)?" } # end writereg # Adding the reg key to bypass the RDP client 'unkown publisher' warning for SPFESO purposes if ($Writereg -eq 'y'){ $RegPath = 'HKCU:\Software\Microsoft\Terminal Server Client\LocalDevices' if (!(Test-Path $RegPath)){ New-Item $RegPath -Force } # end if RegPath Set-ItemProperty -Path 'HKCU:\Software\Microsoft\Terminal Server Client\LocalDevices' -Name $RDPfqdn -Value 76 -type dword -Force } # end writereg y "`nFile '$RDPfilepath' generated with saved encrypted credentials." } else { "`nFile '$RDPfilepath' generated without saved credentials." } # end if nopass,vmcred # Launch Remote Desktop connection if the nolaunch parameter isn't specified if (!$NoLaunch){ mstsc $RDPfilepath } # end if NoLaunch # Deletes the RDP file when the tempory location is used and otherwise warn if the RDP file with password is saved in a given directory. if (!$Directory){ write-host "Temporary location used, deleting RDP file ..." -NoNewline sleep 2 try { del $RDPfilepath -Force -Confirm:$False } catch { "Failed to delete the $RDPfilepath file, please delete the file manually." break } # end Try/Catch " deleted '$RDPfilepath'." } elseif (!$NoPass -and $vmcred) { "`n" write-warning "Added password as a secure string in the RDP file for direct logon demo purposes. If you don't want to save the password (possible decryption) then use the '-NoPass' parameter." } # end if directory/nopass/vmcred } # end foreach VMs } # end shouldprocess } # end process block } # end function Generate-RDPFile Function Global:Delete-AzureVM { <# .SYNOPSIS Deletes one or more VM's. .DESCRIPTION Deletes the VM and related services from Azure. .PARAMETER Name One or more Virtual Machines to operate against. Accepts pipeline input ByValue and ByPropertyName. .EXAMPLE Delete-AzureVM -Retrieve Executes the slow query with 'Get-AzureVM' to retrieve all VM's live from Azure instead of using the indexed ones created with this script. .EXAMPLE Select-VM | Delete-AzureVM Allows you to select and delete the VM's created by this script. .LINK www.ruudborst.nl #> [CmdletBinding(DefaultParametersetName="2",SupportsShouldProcess = $True,ConfirmImpact = 'High')] Param( [Parameter(ParameterSetName = "1",ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True,ValueFromRemainingArguments=$true,HelpMessage='Enter the VM name')] [Alias('ComputerName','MachineName','Server','VM')] [ValidateLength(3,15)] [string[]]$Name, [Parameter(ParameterSetName = "2",HelpMessage = "Specify parameter as a switch to retrieve the VM's from Azure")] [Switch]$Retrieve ) BEGIN { Write-Host "`n - [$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan Write-Host "This function deletes the VM, related services and attached disks for the virtual machines specified. You will receive a delete confirmation request for each Virtual Machine.`n" Pause # Create the removevms array to store the deleted VM's in an array and process them in the END block $RemovedVMs = @() # Instantiate the VM to Location Hash table if it doesn't exist if (!$VMtoLoc){$Global:VMtoLoc = @{}} } # end begin block PROCESS { # Warn and break if no VM name or retrieve parameter is specified if (!$Name -and !$Retrieve) { "`n";Write-Warning -Message "No input received for the Name parameter, please specify the '-Retrieve' parameter to retrieve the VM's from Azure and delete the selected ones." break # Else if name is not specified but retrieve is then execute get-azurevm to retrieve a list to choose from } elseif (!$Name -and $Retrieve) { Write-Host "`nRetrieve parameter specified, retrieving VM's from Azure using 'Get-AzureVM' ..." $VMs=(Get-AzureVM).Name if ($VMs) { try { $Name = $VMs | Out-GridView -Title "Please select one or more VM's for deletion" -PassThru }catch{ # do nothing } # end try/catch } else { "`nNo VM's found in Azure, try to create a VM by running this script again or use the function, for example: 'Create-AzureVM VM1'." break } # end if if (!$Name) { "No VM's selected, exiting function ..." break } # end if name } # end if name,retrieve if ($pscmdlet.ShouldProcess($Name)) { if ($Name){ # Delete the VM, Azure Service and DISK/VHD. Add the VM to the RemovedVMs array for processing in the END block $Name | ForEach { Write-Host "`n[$_ | $($VMtoLoc[$_])]`n" -ForegroundColor DarkCyan Get-AzureVM $_ | Remove-AzureVM -DeleteVHD:$True -verbose Remove-AzureService -ServiceName $_ -force:$True -Verbose $RemovedVMs += $_ } # end foreach azurevm } # end if Name } # end shouldprocess } # end process block END { # Removes the VM from all VM info hashtables $RemovedVMs | % { try{$VMtoLoc.remove($_)}catch{} try{$VMtoUri.remove($_)}catch{} try{$VMtoRDP.remove($_)}catch{} try{$VMtoIP.remove($_)}catch{} try{$WinRMFwInfo.remove($_)}catch{} } # end foreach } # end end block } # end Delete-AzureVM function Function Global:Delete-AzureStorageAccount { <# .SYNOPSIS Deletes one or more storage accounts. .DESCRIPTION Deletes the empty storage account in Azure. .PARAMETER Name This is the fully written Storage Account Name, accepts pipeline input ByValue and ByPropertyName. .EXAMPLE Delete-AzureStorageAccount -Retrieve .EXAMPLE Delete-AzureStorageAccount stornorthcentralus .LINK www.ruudborst.nl #> [CmdletBinding(DefaultParametersetName="2",SupportsShouldProcess = $True, ConfirmImpact = 'High')] Param( [Parameter(ParameterSetName = "1",ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True,ValueFromRemainingArguments=$true,HelpMessage = 'Enter the storage account name')] [Alias('StorageAccount')] [ValidateLength(3,24)] [ValidateScript({$_ -cmatch "^[a-z0-9]*$"})] [string[]]$Name, [Parameter(ParameterSetName = "2",HelpMessage = 'Specify to retrieve the storage accounts from Azure')] [Switch]$Retrieve ) BEGIN { Write-Host "`n - [$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan "This function deletes empty storage accounts, make sure there is no data in the account otherwise the deletion will fail." Pause # Create the RemovedSTa array to store the deleted storage accounts in an array and process them in the END block $RemovedSTa = @() } # end begin block PROCESS { # Warn and break if no Storage account name or retrieve parameter is specified if (!$Name -and !$Retrieve) { "`n";Write-Warning -Message "No input received for the name parameter, please specify a storage account name or use the '-Retrieve' parameter to retrieve the Storage Account's from Azure and select the ones you want to delete." break # Else if name is not specified but retrieve is then execute Get-AzureStorageAccount to retrieve a list to choose from } elseif (!$Name -and $Retrieve) { write-host "`nRetrieve parameter specified, retrieving accounts from Azure ..." $Name = (Get-AzureStorageAccount -WarningAction SilentlyContinue | select StorageAccountName, Label, GeoPrimaryLocation | Out-GridView -Title 'Please select one or more storage accounts to delete' -PassThru).StorageAccountName if (!$Name) { write-host "No Storage Account's selected, aborting function ...`n" break } # end if StorageAccountName } # end if StorageAccountName,Retrieve if ($pscmdlet.ShouldProcess($Name)) { if ($Name){ # Delete the storage account and add the account to the RemovedSTa array for processing in the END block $Name | ForEach -Process { Write-Host "`n[$_]`n" -ForegroundColor DarkCyan try { Remove-AzureStorageAccount -StorageAccountName $_ -ErrorAction Stop -Verbose $RemovedSTa += $_ } catch { write-warning $_.exception.message write-warning 'Could not delete storageaccount, make sure the account is empty or wait for Azure to remove the VM files from the account and try again.' } # end catch } # end foreach Name } # end if Name } # end shouldprocess } # end process block END { # Removes the Storage Account from the storage account to location hashtable $RemovedSTa | ForEach -Process { try{ $STtoLoc.remove($_) }catch{ # do nothing } # end try/catch } # end foreach RemovedSTa } # end end block } # end Delete-AzureStorageAccount function Function Global:Select-VM { <# .SYNOPSIS Select on or more script indexed Azure VM's. .DESCRIPTION Allows you to select one or more newly created VM's by this script and select their names via the out-gridview cmdlet and output them as an array. All VM's are returned by supplying the 'All' parameter. .PARAMETER All Returns all VM's as an array instead of selecting them in a gridview. .EXAMPLE Select-VM .EXAMPLE Select-VM | Delete-AzureVM .EXAMPLE Select-VM -All | Execute-RemoteCommand .EXAMPLE Execute-RemoteCommand (Select-VM) .LINK www.ruudborst.nl #> Param([switch]$All) # Returns all indexed VM's as an array when the All parameter is supplied # Else if the parameter isn't specified let the user select one or more indexed VM's # Else if their are no indexed VM's (not created in this session) let the user choose to execute the index-vminfo and retrieve all VM's from Azure if ($VMtoLoc.count -gt 0 -and $All) { [array]$VMarray = $VMtoLoc.keys $VMarray } elseif ($VMtoLoc.count -gt 0) { ($VMtoLoc | Out-GridView -PassThru -Title 'Select VM').name } else { Write-Host "`n - [$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan write-Host "No VM's indexed, executing 'Index-VMinfo -All' to index all available VM's in Azure.`n" Index-VMinfo -All Select-VM } # end if VMtoLoc } # end function select-vm Function Global:Pause { $Message = "`nPress any key to continue ..." # Determine if we're running the Console or ISE, ISE gives an error if you use the $host.UI.RawUI.ReadKey method if ($Host.name -eq 'ConsoleHost') { $Message $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') }else { $null = Read-Host -Prompt $Message } # end if } # end pause function # # Scriptblocks ## # Performs a .NET webclient HTTP connect to google and measures the response. # This scriptblock is used as the default scriptblock in the 'execute-remotecommand' function. $Global:HttpConnect = { if (!$webclient) { # Change to your url $url = 'http://www.google.com' $webclient = New-Object System.Net.WebCLient $hostname = $env:COMPUTERNAME } # end if webclient check $IPaddreses = (Get-WmiObject -Class Win32_NetworkAdapterConfiguration).ipaddress $IP = ($IPaddreses | Where{ $_ -match '\d' } )[0] $MS = [math]::round((Measure-Command -Expression { $Result = $webclient.DownloadString($url) } ).totalmilliseconds,2) if ($Result -match 'google'){ [pscustomobject]@{ VM = $hostname Url = $url HttpConnect = 'Success' DurationinMS = $MS } } else { [pscustomobject]@{ VM = $hostname URL = $url HttpConnect = 'Failed' ConnectDurationinMS = $null } } # end if result } # end httpconnect scriptblock # Retrieves the VM Hyper-V through the default integration services set values $Global:HyperVHost = {(Get-Item "HKLM:\SOFTWARE\Microsoft\Virtual Machine\Guest\Parameters").GetValue("HostName")} # # Workflows ## # These workflows are used in the main routine and aid in the batch creation of the VM's and storageaccounts by creating parallel PS workflow threads/runspaces Workflow WFCreate-StorageAccounts { param($StorageAccounts) foreach -parallel($StorageAccount in $StorageAccounts.keys) { Create-AzureStorageAccount -StorageAccountName $StorageAccount -Location $StorageAccounts[$StorageAccount] } # end foreach AzureVMS parallel } # end WFCreate-StorageAccounts Workflow WFCreate-AzureVMs { param($AzureVMS,$VMcred,$ImageName,$InstanceSize) foreach -parallel($AzureVM in $AzureVMS.keys) { # Built-in sleep, this we way PS workflow doesn't run into the dreadful AzureProfile.json being used by another process # when switching the storage account in the active subscription, the change happens in the profile file which can't be accessed by multiple processes/threads. $WaitDelay = 400 $Index = [array]::indexof($AzureVMS.keys,$AzureVM) if ($Index -in 0..4){ $SleepinMS = $Index * $WaitDelay } elseif ($Index -in 5..9) { $SleepinMS = ($Index - 5) * $WaitDelay } elseif ($Index -in 10..15) { $SleepinMS = ($Index - 10) * $WaitDelay } # end if Index Create-AzureVM -VM $AzureVM -StorageAccountName $AzureVMS[$AzureVM] -Credential $VMcred -ImageName $ImageName -InstanceSize $InstanceSize -SleepinMS $SleepinMS } # end foreach AzureVMS parallel } # end WFCreate-AzureVMs } # end main begin block PROCESS { # # Main routine ## Write-Host "`n[$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan "This script provisions an Azure VM, Azure Service and tied Storage Account in each available Microsoft Azure Region.`nUse 'Get-Help $($myinvocation.MyCommand) -Full' for more information or visit the blog post on http://www.ruudborst.nl" # Scoping instancesize to global for reuse in 'Create-AzureVM' when breaking the script at this point. $Global:InstanceSize = $InstanceSize # Wait for user consent by pressing any key and start collecting input Pause ; Sleep 1 # Create a splat and adds the script parameter to use in the ConnectTo-Azure function, it selects a existing subscription or imports a new subscription. # The importpubfile parameter uses the import certificate publish file method 'Import-AzurePublishSettingsFile', # it overcomes the 12 hour expiring session of the default script chosen 'add-azureaccount' method. $Null = $PSBoundParameters.remove("VMPrefix") $Null = $PSBoundParameters.remove("StorageAccountPrefix") $Null = $PSBoundParameters.remove("InstanceSize") ConnectTo-Azure @PSBoundParameters Write-Host "[$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan # Retrieves the available deployment images from Azure and their respective image family's. # It selects the latest image for the chosen image family and stores it in the ImageName variable. Write-Host " - [Retrieve-Image]`n" -ForegroundColor Cyan Write-Host "Retrieving Azure VM images ...`n" $null = Retrieve-Image Write-Host "Found image '$ImageName'" Sleep 1 Write-Host "`n[$($myinvocation.MyCommand)]" -ForegroundColor Cyan # Sets the available Azure Datacenter locations containing the 'Compute' resource to an array. $Locations = (Get-AzureLocation | where availableservices -Contains 'Compute').Name # Prompts for the Datacenter locations where the VM should be deployed $ChosenLocations = $Locations | Out-GridView -PassThru -Title "Select Azure regions. A VM and Storage Account will be created in each region." write-host "`nUsing the chosen regions: " -NoNewline write-host $ChosenLocations -ForegroundColor DarkCyan -Separator ", " -NoNewline ;'.' # Prompts for the Azure VM Windows credential, username 'Administrator' or 'Admin' is prohibited. $Global:VMcred = Get-Credential -UserName "AzureAdmin" -Message "Please supply a Windows administrator account username and complex password.`n Username 'Administrator' or 'Admin' is prohibited." if ($VMcred.username -eq 'Administrator' -or $VMcred.username -eq 'Admin') { Write-Host 'Username Administrator or Admin is prohibited, please choose another username.' Pause $Global:VMcred = Get-Credential -Message "Please supply the Azure Virtual Machine Windows Admin`n username and password." } # end if $VMcred.username check # Regex password check matched with Azure's password policy, reprompt user for password when it doesn't match. if ($VMcred.GetNetworkCredential().Password -notmatch "^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,30}$") { write-warning 'Password does not meet complexity requirements. Password must be at least 8 characters in length and must contain one UPPER case character, one lower case character and one number.' Pause $Global:VMcred = Get-Credential -UserName $VMcred.username -Message "Please supply a complex password witn a minimum length of 8 characters." } # end if VMcred.password check # VMprefix combined with the Azure location/region short name forms the unique Azure DNS name of the VM. # Since it has to be unique, get-random is used when the VMprefix is blank. $Rand = (Get-Random -Minimum 100 -Maximum 999) if (!$VMprefix) { $VMprefix = "DEMO$Rand-" } # end if VMprefix # StorageAccountPrefix combined with the location short name forms the unique Azure DNS name of the StorageAccount. # Since it has to be unique, get-random is used when the StorageAccountPrefix is left empty. if (!$StorageAccountPrefix) { $StorageAccountPrefix = "demo$Rand" } # end if StorageAccountPrefix # Generating VM and storage account names combined with the chosen locations and prefixes. # All data is stored in hashtables and used in the workflow execution below. if ($ChosenLocations -and $VMcred -and $ImageName ) { # Create the provisioning hashtables $VMsCount = 0 $Global:StorageAccounts = @{} $Global:AzureVMs = @{} # The foreach combines each chosen location with the relevant # unique prefix to form the short VM name and long lowercase # storage account name and stores them in the provisioning hashtables. $ChosenLocations | ForEach -Process { # Sets VM name based on the location without any spaces in it $Location = $_ $Name = $Location -replace ' ', '' $VMname = $null $Location -split ' ' | ForEach -Process { $VMname += $_.substring(0,1) } # end foreach $VMname = $VMprefix + $VMname $StorageAccountName = ($StorageAccountPrefix + $Name).tolower() # Add storage account name,vm and location to the provisioning # hashtables used by the create 'WFCreate-StorageAccounts' and 'WFCreate-AzureVMs' workflows $StorageAccounts[$StorageAccountName] = $Location $AzureVMS[$VMname] = $StorageAccountName } # end foreach chosenlocations 'Generated configuration data.' $CreationStart = Get-Date # Executing the creation of the storage accounts and related VM's through PowerShell's parallel workflow execution. # Parallel workflow execution in PS version 3 is restricted to five threads, for more information see script description. Write-Host "`n - [WFCreate-StorageAccounts]`n" -ForegroundColor Cyan "Starting storage account creation through PowerShell's parallel workflow execution ...`n" WFCreate-StorageAccounts $StorageAccounts Write-Host "`n - [WFCreate-AzureVMs]`n" -ForegroundColor Cyan "Starting VM creation through PowerShell's parallel workflow execution ...`n" WFCreate-AzureVMs $AzureVMS $VMcred $ImageName $InstanceSize $CreationEnd = Get-Date } else { Write-Warning -Message 'Please make sure you supplied the Datacenter location, VM credentials and image family type, exiting ...' } # end if chosenlocations,vmcred,image } # end process block END { # Scoping the script variables to the global scope for script/console re-use purposes $Global:VMprefix = $VMprefix $Global:StorageAccountprefix = $StorageAccountprefix $Global:SubscriptionName = $SubscriptionName # Index the created VM's with WinRM Endpoint information. # WinRM endpoint information is needed for Remote PowerShell commands which are executed via the 'Execute-RemoteCommand' function, # which in turn calls the 'Session-Setup' function and requires the WinRM url to establish the remote PS session through SSL. Index-VMinfo -NoInfo # Checking the successful creation of the VM's and StorageAccounts by querying Azure since we can't get any variables from the workflows [array]$AzureVMs.keys | foreach { if (!$VMtoLoc[$_]){ $AzureVMs.remove($_) } # end if VMtoLoc } # end foreach AzureVMS [array]$StorageAccounts.keys | foreach { if (!$STtoLoc[$_]){ $StorageAccounts.remove($_) } # end if STtoLoc } # end foreach StorageAccounts Write-Host "`n[$($myinvocation.MyCommand)]`n" -ForegroundColor Cyan # Outputs the creation duration and VM count. $VMcreatedCount = $AzureVMs.count # Output the created VM's and Storage Accounts plus script examples and functions to use through 'get-help', that's all folks! if ($VMcreatedCount -ge 1){ $CreationDuration = [System.Math]::Round(($CreationEnd - $CreationStart).TotalMinutes, 2) write-host "Created $VMcreatedCount Azure VM's and tied Storage Accounts in " -NoNewline write-host "$CreationDuration minutes." -ForegroundColor Green $AzureVMS.keys | select @{Name="VM";Expression={$_}},@{Name="Location";Expression={$StorageAccounts[$AzureVMS[$_]]}},@{Name="StorageAccount";Expression={$AzureVMS[$_]}} | Format-Table Pause write-host "`nUse the imported script functions below to manage the VM's. For more information about a specific function use 'Get-Help FunctionName -Full'." # Retrieve all script functions matching the unique link with 'Get-Help' and output the name and description $FunctionHelp = get-help -Category Function | ? {$_.relatedlinks -match "www.ruudborst.nl"} $FunctionHelp | sort name| ft @{Name="Function";Expression={$_.name}},Synopsis,@{Name="Example";Expression={$_.examples.example.code[0]}} -AutoSize # Displaying some examples, 'write-host' needed for nonewline and coloring purposes, don't shoot me :) write-host 'Example: ' -NoNewline write-host 'Select-VM | Execute-RemoteCommand' -ForegroundColor DarkCyan write-host 'Example: ' -NoNewline write-host 'Select-VM -All | Execute-RemoteCommand -sb {(measure-command {iwr ams-ix.net -usebasicparsing}).totalseconds}' -ForegroundColor darkcyan write-host 'Example: ' -NoNewline write-host 'Generate-RDPFile (Select-VM)' -ForegroundColor darkcyan write-host 'Example: ' -NoNewline write-host 'Select-VM | Generate-RDPFile -Directory ''c:\RDPfiles'' -NoLaunch' -ForegroundColor darkcyan write-host 'Example: ' -NoNewline write-host '@(''UniqueVMname1'',''UniqueVMname2'') | Create-AzureVM' -ForegroundColor darkcyan write-host 'Example: ' -NoNewline write-host 'Cleanup-Demo' -ForegroundColor darkcyan } # end if VMcreatedCount } # end end block |
Feedback
I really, realllyyy want to have your feedback, let me know if you found a bug, have a improvement for me or if you just want to ask why I did specific things. I’ll update the script dependent on this reception and feedback so leave a comment or e-mail me! And while you’re at it follow me on Twitter! I promise, I won’t spam you with simple, useless or ‘old news’ tweets. Thank you for taking the time to read this post, I did my very best to keep it as compact, clear and simple as possible which was quite a challenge. J