1 using System;
2 using System.Collections;
3 using System.Collections.Generic;
4 using System.Diagnostics;
5 using System.IO;
6 using System.Linq;
7 using System.Management.Automation;
8 using System.Management.Automation.Runspaces;
9 using System.Reflection;
10 using System.Runtime.InteropServices;
11 using System.Security.AccessControl;
12 using System.Security.Principal;
13 #if CORECLR
14 using Newtonsoft.Json;
15 #else
16 using System.Web.Script.Serialization;
17 #endif
18 
19 // System.Diagnostics.EventLog.dll reference different versioned dlls that are
20 // loaded in PSCore, ignore CS1702 so the code will ignore this warning
21 //NoWarn -Name CS1702 -CLR Core
22 
23 //AssemblyReference -Type Newtonsoft.Json.JsonConvert -CLR Core
24 //AssemblyReference -Type System.Diagnostics.EventLog -CLR Core
25 //AssemblyReference -Type System.Security.AccessControl.NativeObjectSecurity -CLR Core
26 //AssemblyReference -Type System.Security.AccessControl.DirectorySecurity -CLR Core
27 //AssemblyReference -Type System.Security.Principal.IdentityReference -CLR Core
28 
29 //AssemblyReference -Name System.Web.Extensions.dll -CLR Framework
30 
31 namespace Ansible.Basic
32 {
33     public class AnsibleModule
34     {
ExitHandler(int rc)35         public delegate void ExitHandler(int rc);
36         public static ExitHandler Exit = new ExitHandler(ExitModule);
37 
WriteLineHandler(string line)38         public delegate void WriteLineHandler(string line);
39         public static WriteLineHandler WriteLine = new WriteLineHandler(WriteLineModule);
40 
41         public static bool _DebugArgSpec = false;
42 
43         private static List<string> BOOLEANS_TRUE = new List<string>() { "y", "yes", "on", "1", "true", "t", "1.0" };
44         private static List<string> BOOLEANS_FALSE = new List<string>() { "n", "no", "off", "0", "false", "f", "0.0" };
45 
46         private string remoteTmp = Path.GetTempPath();
47         private string tmpdir = null;
48         private HashSet<string> noLogValues = new HashSet<string>();
49         private List<string> optionsContext = new List<string>();
50         private List<string> warnings = new List<string>();
51         private List<Dictionary<string, string>> deprecations = new List<Dictionary<string, string>>();
52         private List<string> cleanupFiles = new List<string>();
53 
54         private Dictionary<string, string> passVars = new Dictionary<string, string>()
55         {
56             // null values means no mapping, not used in Ansible.Basic.AnsibleModule
57             { "check_mode", "CheckMode" },
58             { "debug", "DebugMode" },
59             { "diff", "DiffMode" },
60             { "keep_remote_files", "KeepRemoteFiles" },
61             { "module_name", "ModuleName" },
62             { "no_log", "NoLog" },
63             { "remote_tmp", "remoteTmp" },
64             { "selinux_special_fs", null },
65             { "shell_executable", null },
66             { "socket", null },
67             { "string_conversion_action", null },
68             { "syslog_facility", null },
69             { "tmpdir", "tmpdir" },
70             { "verbosity", "Verbosity" },
71             { "version", "AnsibleVersion" },
72         };
73         private List<string> passBools = new List<string>() { "check_mode", "debug", "diff", "keep_remote_files", "no_log" };
74         private List<string> passInts = new List<string>() { "verbosity" };
75         private Dictionary<string, List<object>> specDefaults = new Dictionary<string, List<object>>()
76         {
77             // key - (default, type) - null is freeform
78             { "apply_defaults", new List<object>() { false, typeof(bool) } },
79             { "aliases", new List<object>() { typeof(List<string>), typeof(List<string>) } },
80             { "choices", new List<object>() { typeof(List<object>), typeof(List<object>) } },
81             { "default", new List<object>() { null, null } },
82             { "deprecated_aliases", new List<object>() { typeof(List<Hashtable>), typeof(List<Hashtable>) } },
83             { "elements", new List<object>() { null, null } },
84             { "mutually_exclusive", new List<object>() { typeof(List<List<string>>), typeof(List<object>) } },
85             { "no_log", new List<object>() { false, typeof(bool) } },
86             { "options", new List<object>() { typeof(Hashtable), typeof(Hashtable) } },
87             { "removed_in_version", new List<object>() { null, typeof(string) } },
88             { "removed_at_date", new List<object>() { null, typeof(DateTime) } },
89             { "removed_from_collection", new List<object>() { null, typeof(string) } },
90             { "required", new List<object>() { false, typeof(bool) } },
91             { "required_by", new List<object>() { typeof(Hashtable), typeof(Hashtable) } },
92             { "required_if", new List<object>() { typeof(List<List<object>>), typeof(List<object>) } },
93             { "required_one_of", new List<object>() { typeof(List<List<string>>), typeof(List<object>) } },
94             { "required_together", new List<object>() { typeof(List<List<string>>), typeof(List<object>) } },
95             { "supports_check_mode", new List<object>() { false, typeof(bool) } },
96             { "type", new List<object>() { "str", null } },
97         };
98         private Dictionary<string, Delegate> optionTypes = new Dictionary<string, Delegate>()
99         {
100             { "bool", new Func<object, bool>(ParseBool) },
101             { "dict", new Func<object, Dictionary<string, object>>(ParseDict) },
102             { "float", new Func<object, float>(ParseFloat) },
103             { "int", new Func<object, int>(ParseInt) },
104             { "json", new Func<object, string>(ParseJson) },
105             { "list", new Func<object, List<object>>(ParseList) },
106             { "path", new Func<object, string>(ParsePath) },
107             { "raw", new Func<object, object>(ParseRaw) },
108             { "sid", new Func<object, SecurityIdentifier>(ParseSid) },
109             { "str", new Func<object, string>(ParseStr) },
110         };
111 
112         public Dictionary<string, object> Diff = new Dictionary<string, object>();
113         public IDictionary Params = null;
114         public Dictionary<string, object> Result = new Dictionary<string, object>() { { "changed", false } };
115 
116         public bool CheckMode { get; private set; }
117         public bool DebugMode { get; private set; }
118         public bool DiffMode { get; private set; }
119         public bool KeepRemoteFiles { get; private set; }
120         public string ModuleName { get; private set; }
121         public bool NoLog { get; private set; }
122         public int Verbosity { get; private set; }
123         public string AnsibleVersion { get; private set; }
124 
125         public string Tmpdir
126         {
127             get
128             {
129                 if (tmpdir == null)
130                 {
131                     SecurityIdentifier user = WindowsIdentity.GetCurrent().User;
132                     DirectorySecurity dirSecurity = new DirectorySecurity();
133                     dirSecurity.SetOwner(user);
134                     dirSecurity.SetAccessRuleProtection(true, false);  // disable inheritance rules
135                     FileSystemAccessRule ace = new FileSystemAccessRule(user, FileSystemRights.FullControl,
136                         InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
137                         PropagationFlags.None, AccessControlType.Allow);
138                     dirSecurity.AddAccessRule(ace);
139 
140                     string baseDir = Path.GetFullPath(Environment.ExpandEnvironmentVariables(remoteTmp));
141                     if (!Directory.Exists(baseDir))
142                     {
143                         string failedMsg = null;
144                         try
145                         {
146 #if CORECLR
147                             DirectoryInfo createdDir = Directory.CreateDirectory(baseDir);
148                             FileSystemAclExtensions.SetAccessControl(createdDir, dirSecurity);
149 #else
150                             Directory.CreateDirectory(baseDir, dirSecurity);
151 #endif
152                         }
153                         catch (Exception e)
154                         {
155                             failedMsg = String.Format("Failed to create base tmpdir '{0}': {1}", baseDir, e.Message);
156                         }
157 
158                         if (failedMsg != null)
159                         {
160                             string envTmp = Path.GetTempPath();
161                             Warn(String.Format("Unable to use '{0}' as temporary directory, falling back to system tmp '{1}': {2}", baseDir, envTmp, failedMsg));
162                             baseDir = envTmp;
163                         }
164                         else
165                         {
166                             NTAccount currentUser = (NTAccount)user.Translate(typeof(NTAccount));
167                             string warnMsg = String.Format("Module remote_tmp {0} did not exist and was created with FullControl to {1}, ", baseDir, currentUser.ToString());
168                             warnMsg += "this may cause issues when running as another user. To avoid this, create the remote_tmp dir with the correct permissions manually";
169                             Warn(warnMsg);
170                         }
171                     }
172 
173                     string dateTime = DateTime.Now.ToFileTime().ToString();
174                     string dirName = String.Format("ansible-moduletmp-{0}-{1}", dateTime, new Random().Next(0, int.MaxValue));
175                     string newTmpdir = Path.Combine(baseDir, dirName);
176 #if CORECLR
177                     DirectoryInfo tmpdirInfo = Directory.CreateDirectory(newTmpdir);
178                     FileSystemAclExtensions.SetAccessControl(tmpdirInfo, dirSecurity);
179 #else
180                     Directory.CreateDirectory(newTmpdir, dirSecurity);
181 #endif
182                     tmpdir = newTmpdir;
183 
184                     if (!KeepRemoteFiles)
185                         cleanupFiles.Add(tmpdir);
186                 }
187                 return tmpdir;
188             }
189         }
190 
AnsibleModule(string[] args, IDictionary argumentSpec, IDictionary[] fragments = null)191         public AnsibleModule(string[] args, IDictionary argumentSpec, IDictionary[] fragments = null)
192         {
193             // NoLog is not set yet, we cannot rely on FailJson to sanitize the output
194             // Do the minimum amount to get this running before we actually parse the params
195             Dictionary<string, string> aliases = new Dictionary<string, string>();
196             try
197             {
198                 ValidateArgumentSpec(argumentSpec);
199 
200                 // Merge the fragments if present into the main arg spec.
201                 if (fragments != null)
202                 {
203                     foreach (IDictionary fragment in fragments)
204                     {
205                         ValidateArgumentSpec(fragment);
206                         MergeFragmentSpec(argumentSpec, fragment);
207                     }
208                 }
209 
210                 // Used by ansible-test to retrieve the module argument spec, not designed for public use.
211                 if (_DebugArgSpec)
212                 {
213                     // Cannot call exit here because it will be caught with the catch (Exception e) below. Instead
214                     // just throw a new exception with a specific message and the exception block will handle it.
215                     ScriptBlock.Create("Set-Variable -Name ansibleTestArgSpec -Value $args[0] -Scope Global"
216                         ).Invoke(argumentSpec);
217                     throw new Exception("ansible-test validate-modules check");
218                 }
219 
220                 // Now make sure all the metadata keys are set to their defaults, this must be done after we've
221                 // potentially output the arg spec for ansible-test.
222                 SetArgumentSpecDefaults(argumentSpec);
223 
224                 Params = GetParams(args);
225                 aliases = GetAliases(argumentSpec, Params);
226                 SetNoLogValues(argumentSpec, Params);
227             }
228             catch (Exception e)
229             {
230                 if (e.Message == "ansible-test validate-modules check")
231                     Exit(0);
232 
233                 Dictionary<string, object> result = new Dictionary<string, object>
234                 {
235                     { "failed", true },
236                     { "msg", String.Format("internal error: {0}", e.Message) },
237                     { "exception", e.ToString() }
238                 };
239                 WriteLine(ToJson(result));
240                 Exit(1);
241             }
242 
243             // Initialise public properties to the defaults before we parse the actual inputs
244             CheckMode = false;
245             DebugMode = false;
246             DiffMode = false;
247             KeepRemoteFiles = false;
248             ModuleName = "undefined win module";
249             NoLog = (bool)argumentSpec["no_log"];
250             Verbosity = 0;
251             AppDomain.CurrentDomain.ProcessExit += CleanupFiles;
252 
253             List<string> legalInputs = passVars.Keys.Select(v => "_ansible_" + v).ToList();
254             legalInputs.AddRange(((IDictionary)argumentSpec["options"]).Keys.Cast<string>().ToList());
255             legalInputs.AddRange(aliases.Keys.Cast<string>().ToList());
256             CheckArguments(argumentSpec, Params, legalInputs);
257 
258             // Set a Ansible friendly invocation value in the result object
259             Dictionary<string, object> invocation = new Dictionary<string, object>() { { "module_args", Params } };
260             Result["invocation"] = RemoveNoLogValues(invocation, noLogValues);
261 
262             if (!NoLog)
263                 LogEvent(String.Format("Invoked with:\r\n  {0}", FormatLogData(Params, 2)), sanitise: false);
264         }
265 
Create(string[] args, IDictionary argumentSpec, IDictionary[] fragments = null)266         public static AnsibleModule Create(string[] args, IDictionary argumentSpec, IDictionary[] fragments = null)
267         {
268             return new AnsibleModule(args, argumentSpec, fragments);
269         }
270 
Debug(string message)271         public void Debug(string message)
272         {
273             if (DebugMode)
274                 LogEvent(String.Format("[DEBUG] {0}", message));
275         }
276 
Deprecate(string message, string version)277         public void Deprecate(string message, string version)
278         {
279             Deprecate(message, version, null);
280         }
281 
Deprecate(string message, string version, string collectionName)282         public void Deprecate(string message, string version, string collectionName)
283         {
284             deprecations.Add(new Dictionary<string, string>() {
285                 { "msg", message }, { "version", version }, { "collection_name", collectionName } });
286             LogEvent(String.Format("[DEPRECATION WARNING] {0} {1}", message, version));
287         }
288 
Deprecate(string message, DateTime date)289         public void Deprecate(string message, DateTime date)
290         {
291             Deprecate(message, date, null);
292         }
293 
Deprecate(string message, DateTime date, string collectionName)294         public void Deprecate(string message, DateTime date, string collectionName)
295         {
296             string isoDate = date.ToString("yyyy-MM-dd");
297             deprecations.Add(new Dictionary<string, string>() {
298                 { "msg", message }, { "date", isoDate }, { "collection_name", collectionName } });
299             LogEvent(String.Format("[DEPRECATION WARNING] {0} {1}", message, isoDate));
300         }
301 
ExitJson()302         public void ExitJson()
303         {
304             WriteLine(GetFormattedResults(Result));
305             CleanupFiles(null, null);
306             Exit(0);
307         }
308 
FailJson(string message)309         public void FailJson(string message) { FailJson(message, null, null); }
FailJson(string message, ErrorRecord psErrorRecord)310         public void FailJson(string message, ErrorRecord psErrorRecord) { FailJson(message, psErrorRecord, null); }
FailJson(string message, Exception exception)311         public void FailJson(string message, Exception exception) { FailJson(message, null, exception); }
FailJson(string message, ErrorRecord psErrorRecord, Exception exception)312         private void FailJson(string message, ErrorRecord psErrorRecord, Exception exception)
313         {
314             Result["failed"] = true;
315             Result["msg"] = RemoveNoLogValues(message, noLogValues);
316 
317 
318             if (!Result.ContainsKey("exception") && (Verbosity > 2 || DebugMode))
319             {
320                 if (psErrorRecord != null)
321                 {
322                     string traceback = String.Format("{0}\r\n{1}", psErrorRecord.ToString(), psErrorRecord.InvocationInfo.PositionMessage);
323                     traceback += String.Format("\r\n    + CategoryInfo          : {0}", psErrorRecord.CategoryInfo.ToString());
324                     traceback += String.Format("\r\n    + FullyQualifiedErrorId : {0}", psErrorRecord.FullyQualifiedErrorId.ToString());
325                     traceback += String.Format("\r\n\r\nScriptStackTrace:\r\n{0}", psErrorRecord.ScriptStackTrace);
326                     Result["exception"] = traceback;
327                 }
328                 else if (exception != null)
329                     Result["exception"] = exception.ToString();
330             }
331 
332             WriteLine(GetFormattedResults(Result));
333             CleanupFiles(null, null);
334             Exit(1);
335         }
336 
LogEvent(string message, EventLogEntryType logEntryType = EventLogEntryType.Information, bool sanitise = true)337         public void LogEvent(string message, EventLogEntryType logEntryType = EventLogEntryType.Information, bool sanitise = true)
338         {
339             if (NoLog)
340                 return;
341 
342             string logSource = "Ansible";
343             bool logSourceExists = false;
344             try
345             {
346                 logSourceExists = EventLog.SourceExists(logSource);
347             }
348             catch (System.Security.SecurityException) { }  // non admin users may not have permission
349 
350             if (!logSourceExists)
351             {
352                 try
353                 {
354                     EventLog.CreateEventSource(logSource, "Application");
355                 }
356                 catch (System.Security.SecurityException)
357                 {
358                     // Cannot call Warn as that calls LogEvent and we get stuck in a loop
359                     warnings.Add(String.Format("Access error when creating EventLog source {0}, logging to the Application source instead", logSource));
360                     logSource = "Application";
361                 }
362             }
363             if (sanitise)
364                 message = (string)RemoveNoLogValues(message, noLogValues);
365             message = String.Format("{0} - {1}", ModuleName, message);
366 
367             using (EventLog eventLog = new EventLog("Application"))
368             {
369                 eventLog.Source = logSource;
370                 try
371                 {
372                     eventLog.WriteEntry(message, logEntryType, 0);
373                 }
374                 catch (System.InvalidOperationException) { }  // Ignore permission errors on the Application event log
375                 catch (System.Exception e)
376                 {
377                     // Cannot call Warn as that calls LogEvent and we get stuck in a loop
378                     warnings.Add(String.Format("Unknown error when creating event log entry: {0}", e.Message));
379                 }
380             }
381         }
382 
Warn(string message)383         public void Warn(string message)
384         {
385             warnings.Add(message);
386             LogEvent(String.Format("[WARNING] {0}", message), EventLogEntryType.Warning);
387         }
388 
FromJson(string json)389         public static object FromJson(string json) { return FromJson<object>(json); }
FromJson(string json)390         public static T FromJson<T>(string json)
391         {
392 #if CORECLR
393             return JsonConvert.DeserializeObject<T>(json);
394 #else
395             JavaScriptSerializer jss = new JavaScriptSerializer();
396             jss.MaxJsonLength = int.MaxValue;
397             jss.RecursionLimit = int.MaxValue;
398             return jss.Deserialize<T>(json);
399 #endif
400         }
401 
ToJson(object obj)402         public static string ToJson(object obj)
403         {
404             // Using PowerShell to serialize the JSON is preferable over the native .NET libraries as it handles
405             // PS Objects a lot better than the alternatives. In case we are debugging in Visual Studio we have a
406             // fallback to the other libraries as we won't be dealing with PowerShell objects there.
407             if (Runspace.DefaultRunspace != null)
408             {
409                 PSObject rawOut = ScriptBlock.Create("ConvertTo-Json -InputObject $args[0] -Depth 99 -Compress").Invoke(obj)[0];
410                 return rawOut.BaseObject as string;
411             }
412             else
413             {
414 #if CORECLR
415                 return JsonConvert.SerializeObject(obj);
416 #else
417                 JavaScriptSerializer jss = new JavaScriptSerializer();
418                 jss.MaxJsonLength = int.MaxValue;
419                 jss.RecursionLimit = int.MaxValue;
420                 return jss.Serialize(obj);
421 #endif
422             }
423         }
424 
GetParams(string[] args)425         public static IDictionary GetParams(string[] args)
426         {
427             if (args.Length > 0)
428             {
429                 string inputJson = File.ReadAllText(args[0]);
430                 Dictionary<string, object> rawParams = FromJson<Dictionary<string, object>>(inputJson);
431                 if (!rawParams.ContainsKey("ANSIBLE_MODULE_ARGS"))
432                     throw new ArgumentException("Module was unable to get ANSIBLE_MODULE_ARGS value from the argument path json");
433                 return (IDictionary)rawParams["ANSIBLE_MODULE_ARGS"];
434             }
435             else
436             {
437                 // $complex_args is already a Hashtable, no need to waste time converting to a dictionary
438                 PSObject rawArgs = ScriptBlock.Create("$complex_args").Invoke()[0];
439                 return rawArgs.BaseObject as Hashtable;
440             }
441         }
442 
ParseBool(object value)443         public static bool ParseBool(object value)
444         {
445             if (value.GetType() == typeof(bool))
446                 return (bool)value;
447 
448             List<string> booleans = new List<string>();
449             booleans.AddRange(BOOLEANS_TRUE);
450             booleans.AddRange(BOOLEANS_FALSE);
451 
452             string stringValue = ParseStr(value).ToLowerInvariant().Trim();
453             if (BOOLEANS_TRUE.Contains(stringValue))
454                 return true;
455             else if (BOOLEANS_FALSE.Contains(stringValue))
456                 return false;
457 
458             string msg = String.Format("The value '{0}' is not a valid boolean. Valid booleans include: {1}",
459                 stringValue, String.Join(", ", booleans));
460             throw new ArgumentException(msg);
461         }
462 
ParseDict(object value)463         public static Dictionary<string, object> ParseDict(object value)
464         {
465             Type valueType = value.GetType();
466             if (valueType == typeof(Dictionary<string, object>))
467                 return (Dictionary<string, object>)value;
468             else if (value is IDictionary)
469                 return ((IDictionary)value).Cast<DictionaryEntry>().ToDictionary(kvp => (string)kvp.Key, kvp => kvp.Value);
470             else if (valueType == typeof(string))
471             {
472                 string stringValue = (string)value;
473                 if (stringValue.StartsWith("{") && stringValue.EndsWith("}"))
474                     return FromJson<Dictionary<string, object>>((string)value);
475                 else if (stringValue.IndexOfAny(new char[1] { '=' }) != -1)
476                 {
477                     List<string> fields = new List<string>();
478                     List<char> fieldBuffer = new List<char>();
479                     char? inQuote = null;
480                     bool inEscape = false;
481                     string field;
482 
483                     foreach (char c in stringValue.ToCharArray())
484                     {
485                         if (inEscape)
486                         {
487                             fieldBuffer.Add(c);
488                             inEscape = false;
489                         }
490                         else if (c == '\\')
491                             inEscape = true;
492                         else if (inQuote == null && (c == '\'' || c == '"'))
493                             inQuote = c;
494                         else if (inQuote != null && c == inQuote)
495                             inQuote = null;
496                         else if (inQuote == null && (c == ',' || c == ' '))
497                         {
498                             field = String.Join("", fieldBuffer);
499                             if (field != "")
500                                 fields.Add(field);
501                             fieldBuffer = new List<char>();
502                         }
503                         else
504                             fieldBuffer.Add(c);
505                     }
506 
507                     field = String.Join("", fieldBuffer);
508                     if (field != "")
509                         fields.Add(field);
510 
511                     return fields.Distinct().Select(i => i.Split(new[] { '=' }, 2)).ToDictionary(i => i[0], i => i.Length > 1 ? (object)i[1] : null);
512                 }
513                 else
514                     throw new ArgumentException("string cannot be converted to a dict, must either be a JSON string or in the key=value form");
515             }
516 
517             throw new ArgumentException(String.Format("{0} cannot be converted to a dict", valueType.FullName));
518         }
519 
ParseFloat(object value)520         public static float ParseFloat(object value)
521         {
522             if (value.GetType() == typeof(float))
523                 return (float)value;
524 
525             string valueStr = ParseStr(value);
526             return float.Parse(valueStr);
527         }
528 
ParseInt(object value)529         public static int ParseInt(object value)
530         {
531             Type valueType = value.GetType();
532             if (valueType == typeof(int))
533                 return (int)value;
534             else
535                 return Int32.Parse(ParseStr(value));
536         }
537 
ParseJson(object value)538         public static string ParseJson(object value)
539         {
540             // mostly used to ensure a dict is a json string as it may
541             // have been converted on the controller side
542             Type valueType = value.GetType();
543             if (value is IDictionary)
544                 return ToJson(value);
545             else if (valueType == typeof(string))
546                 return (string)value;
547             else
548                 throw new ArgumentException(String.Format("{0} cannot be converted to json", valueType.FullName));
549         }
550 
ParseList(object value)551         public static List<object> ParseList(object value)
552         {
553             if (value == null)
554                 return null;
555 
556             Type valueType = value.GetType();
557             if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(List<>))
558                 return (List<object>)value;
559             else if (valueType == typeof(ArrayList))
560                 return ((ArrayList)value).Cast<object>().ToList();
561             else if (valueType.IsArray)
562                 return ((object[])value).ToList();
563             else if (valueType == typeof(string))
564                 return ((string)value).Split(',').Select(s => s.Trim()).ToList<object>();
565             else if (valueType == typeof(int))
566                 return new List<object>() { value };
567             else
568                 throw new ArgumentException(String.Format("{0} cannot be converted to a list", valueType.FullName));
569         }
570 
ParsePath(object value)571         public static string ParsePath(object value)
572         {
573             string stringValue = ParseStr(value);
574 
575             // do not validate, expand the env vars if it starts with \\?\ as
576             // it is a special path designed for the NT kernel to interpret
577             if (stringValue.StartsWith(@"\\?\"))
578                 return stringValue;
579 
580             stringValue = Environment.ExpandEnvironmentVariables(stringValue);
581             if (stringValue.IndexOfAny(Path.GetInvalidPathChars()) != -1)
582                 throw new ArgumentException("string value contains invalid path characters, cannot convert to path");
583 
584             // will fire an exception if it contains any invalid chars
585             Path.GetFullPath(stringValue);
586             return stringValue;
587         }
588 
ParseRaw(object value)589         public static object ParseRaw(object value) { return value; }
590 
ParseSid(object value)591         public static SecurityIdentifier ParseSid(object value)
592         {
593             string stringValue = ParseStr(value);
594 
595             try
596             {
597                 return new SecurityIdentifier(stringValue);
598             }
599             catch (ArgumentException) { }  // ignore failures string may not have been a SID
600 
601             NTAccount account = new NTAccount(stringValue);
602             return (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier));
603         }
604 
ParseStr(object value)605         public static string ParseStr(object value) { return value.ToString(); }
606 
ValidateArgumentSpec(IDictionary argumentSpec)607         private void ValidateArgumentSpec(IDictionary argumentSpec)
608         {
609             Dictionary<string, object> changedValues = new Dictionary<string, object>();
610             foreach (DictionaryEntry entry in argumentSpec)
611             {
612                 string key = (string)entry.Key;
613 
614                 // validate the key is a valid argument spec key
615                 if (!specDefaults.ContainsKey(key))
616                 {
617                     string msg = String.Format("argument spec entry contains an invalid key '{0}', valid keys: {1}",
618                         key, String.Join(", ", specDefaults.Keys));
619                     throw new ArgumentException(FormatOptionsContext(msg, " - "));
620                 }
621 
622                 // ensure the value is casted to the type we expect
623                 Type optionType = null;
624                 if (entry.Value != null)
625                     optionType = (Type)specDefaults[key][1];
626                 if (optionType != null)
627                 {
628                     Type actualType = entry.Value.GetType();
629                     bool invalid = false;
630                     if (optionType.IsGenericType && optionType.GetGenericTypeDefinition() == typeof(List<>))
631                     {
632                         // verify the actual type is not just a single value of the list type
633                         Type entryType = optionType.GetGenericArguments()[0];
634                         object[] arrayElementTypes = new object[]
635                         {
636                             null,  // ArrayList does not have an ElementType
637                             entryType,
638                             typeof(object),  // Hope the object is actually entryType or it can at least be casted.
639                         };
640 
641                         bool isArray = entry.Value is IList && arrayElementTypes.Contains(actualType.GetElementType());
642                         if (actualType == entryType || isArray)
643                         {
644                             object rawArray;
645                             if (isArray)
646                                 rawArray = entry.Value;
647                             else
648                                 rawArray = new object[1] { entry.Value };
649 
650                             MethodInfo castMethod = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(entryType);
651                             MethodInfo toListMethod = typeof(Enumerable).GetMethod("ToList").MakeGenericMethod(entryType);
652 
653                             var enumerable = castMethod.Invoke(null, new object[1] { rawArray });
654                             var newList = toListMethod.Invoke(null, new object[1] { enumerable });
655                             changedValues.Add(key, newList);
656                         }
657                         else if (actualType != optionType && !(actualType == typeof(List<object>)))
658                             invalid = true;
659                     }
660                     else
661                         invalid = actualType != optionType;
662 
663                     if (invalid)
664                     {
665                         string msg = String.Format("argument spec for '{0}' did not match expected type {1}: actual type {2}",
666                             key, optionType.FullName, actualType.FullName);
667                         throw new ArgumentException(FormatOptionsContext(msg, " - "));
668                     }
669                 }
670 
671                 // recursively validate the spec
672                 if (key == "options" && entry.Value != null)
673                 {
674                     IDictionary optionsSpec = (IDictionary)entry.Value;
675                     foreach (DictionaryEntry optionEntry in optionsSpec)
676                     {
677                         optionsContext.Add((string)optionEntry.Key);
678                         IDictionary optionMeta = (IDictionary)optionEntry.Value;
679                         ValidateArgumentSpec(optionMeta);
680                         optionsContext.RemoveAt(optionsContext.Count - 1);
681                     }
682                 }
683 
684                 // validate the type and elements key type values are known types
685                 if (key == "type" || key == "elements" && entry.Value != null)
686                 {
687                     Type valueType = entry.Value.GetType();
688                     if (valueType == typeof(string))
689                     {
690                         string typeValue = (string)entry.Value;
691                         if (!optionTypes.ContainsKey(typeValue))
692                         {
693                             string msg = String.Format("{0} '{1}' is unsupported", key, typeValue);
694                             msg = String.Format("{0}. Valid types are: {1}", FormatOptionsContext(msg, " - "), String.Join(", ", optionTypes.Keys));
695                             throw new ArgumentException(msg);
696                         }
697                     }
698                     else if (!(entry.Value is Delegate))
699                     {
700                         string msg = String.Format("{0} must either be a string or delegate, was: {1}", key, valueType.FullName);
701                         throw new ArgumentException(FormatOptionsContext(msg, " - "));
702                     }
703                 }
704             }
705 
706             // Outside of the spec iterator, change the values that were casted above
707             foreach (KeyValuePair<string, object> changedValue in changedValues)
708                 argumentSpec[changedValue.Key] = changedValue.Value;
709         }
710 
MergeFragmentSpec(IDictionary argumentSpec, IDictionary fragment)711         private void MergeFragmentSpec(IDictionary argumentSpec, IDictionary fragment)
712         {
713             foreach (DictionaryEntry fragmentEntry in fragment)
714             {
715                 string fragmentKey = fragmentEntry.Key.ToString();
716 
717                 if (argumentSpec.Contains(fragmentKey))
718                 {
719                     // We only want to add new list entries and merge dictionary new keys and values. Leave the other
720                     // values as is in the argument spec as that takes priority over the fragment.
721                     if (fragmentEntry.Value is IDictionary)
722                     {
723                         MergeFragmentSpec((IDictionary)argumentSpec[fragmentKey], (IDictionary)fragmentEntry.Value);
724                     }
725                     else if (fragmentEntry.Value is IList)
726                     {
727                         IList specValue = (IList)argumentSpec[fragmentKey];
728                         foreach (object fragmentValue in (IList)fragmentEntry.Value)
729                             specValue.Add(fragmentValue);
730                     }
731                 }
732                 else
733                     argumentSpec[fragmentKey] = fragmentEntry.Value;
734             }
735         }
736 
SetArgumentSpecDefaults(IDictionary argumentSpec)737         private void SetArgumentSpecDefaults(IDictionary argumentSpec)
738         {
739             foreach (KeyValuePair<string, List<object>> metadataEntry in specDefaults)
740             {
741                 List<object> defaults = metadataEntry.Value;
742                 object defaultValue = defaults[0];
743                 if (defaultValue != null && defaultValue.GetType() == typeof(Type).GetType())
744                     defaultValue = Activator.CreateInstance((Type)defaultValue);
745 
746                 if (!argumentSpec.Contains(metadataEntry.Key))
747                     argumentSpec[metadataEntry.Key] = defaultValue;
748             }
749 
750             // Recursively set the defaults for any inner options.
751             foreach (DictionaryEntry entry in argumentSpec)
752             {
753                 if (entry.Value == null || entry.Key.ToString() != "options")
754                     continue;
755 
756                 IDictionary optionsSpec = (IDictionary)entry.Value;
757                 foreach (DictionaryEntry optionEntry in optionsSpec)
758                 {
759                     optionsContext.Add((string)optionEntry.Key);
760                     IDictionary optionMeta = (IDictionary)optionEntry.Value;
761                     SetArgumentSpecDefaults(optionMeta);
762                     optionsContext.RemoveAt(optionsContext.Count - 1);
763                 }
764             }
765         }
766 
GetAliases(IDictionary argumentSpec, IDictionary parameters)767         private Dictionary<string, string> GetAliases(IDictionary argumentSpec, IDictionary parameters)
768         {
769             Dictionary<string, string> aliasResults = new Dictionary<string, string>();
770 
771             foreach (DictionaryEntry entry in (IDictionary)argumentSpec["options"])
772             {
773                 string k = (string)entry.Key;
774                 Hashtable v = (Hashtable)entry.Value;
775 
776                 List<string> aliases = (List<string>)v["aliases"];
777                 object defaultValue = v["default"];
778                 bool required = (bool)v["required"];
779 
780                 if (defaultValue != null && required)
781                     throw new ArgumentException(String.Format("required and default are mutually exclusive for {0}", k));
782 
783                 foreach (string alias in aliases)
784                 {
785                     aliasResults.Add(alias, k);
786                     if (parameters.Contains(alias))
787                         parameters[k] = parameters[alias];
788                 }
789 
790                 List<Hashtable> deprecatedAliases = (List<Hashtable>)v["deprecated_aliases"];
791                 foreach (Hashtable depInfo in deprecatedAliases)
792                 {
793                     foreach (string keyName in new List<string> { "name" })
794                     {
795                         if (!depInfo.ContainsKey(keyName))
796                         {
797                             string msg = String.Format("{0} is required in a deprecated_aliases entry", keyName);
798                             throw new ArgumentException(FormatOptionsContext(msg, " - "));
799                         }
800                     }
801                     if (!depInfo.ContainsKey("version") && !depInfo.ContainsKey("date"))
802                     {
803                         string msg = "One of version or date is required in a deprecated_aliases entry";
804                         throw new ArgumentException(FormatOptionsContext(msg, " - "));
805                     }
806                     if (depInfo.ContainsKey("version") && depInfo.ContainsKey("date"))
807                     {
808                         string msg = "Only one of version or date is allowed in a deprecated_aliases entry";
809                         throw new ArgumentException(FormatOptionsContext(msg, " - "));
810                     }
811                     if (depInfo.ContainsKey("date") && depInfo["date"].GetType() != typeof(DateTime))
812                     {
813                         string msg = "A deprecated_aliases date must be a DateTime object";
814                         throw new ArgumentException(FormatOptionsContext(msg, " - "));
815                     }
816                     string collectionName = null;
817                     if (depInfo.ContainsKey("collection_name"))
818                     {
819                         collectionName = (string)depInfo["collection_name"];
820                     }
821                     string aliasName = (string)depInfo["name"];
822 
823                     if (parameters.Contains(aliasName))
824                     {
825                         string msg = String.Format("Alias '{0}' is deprecated. See the module docs for more information", aliasName);
826                         if (depInfo.ContainsKey("version"))
827                         {
828                             string depVersion = (string)depInfo["version"];
829                             Deprecate(FormatOptionsContext(msg, " - "), depVersion, collectionName);
830                         }
831                         if (depInfo.ContainsKey("date"))
832                         {
833                             DateTime depDate = (DateTime)depInfo["date"];
834                             Deprecate(FormatOptionsContext(msg, " - "), depDate, collectionName);
835                         }
836                     }
837                 }
838             }
839 
840             return aliasResults;
841         }
842 
SetNoLogValues(IDictionary argumentSpec, IDictionary parameters)843         private void SetNoLogValues(IDictionary argumentSpec, IDictionary parameters)
844         {
845             foreach (DictionaryEntry entry in (IDictionary)argumentSpec["options"])
846             {
847                 string k = (string)entry.Key;
848                 Hashtable v = (Hashtable)entry.Value;
849 
850                 if ((bool)v["no_log"])
851                 {
852                     object noLogObject = parameters.Contains(k) ? parameters[k] : null;
853                     string noLogString = noLogObject == null ? "" : noLogObject.ToString();
854                     if (!String.IsNullOrEmpty(noLogString))
855                         noLogValues.Add(noLogString);
856                 }
857                 string collectionName = null;
858                 if (v.ContainsKey("removed_from_collection"))
859                 {
860                     collectionName = (string)v["removed_from_collection"];
861                 }
862 
863                 object removedInVersion = v["removed_in_version"];
864                 if (removedInVersion != null && parameters.Contains(k))
865                     Deprecate(String.Format("Param '{0}' is deprecated. See the module docs for more information", k),
866                               removedInVersion.ToString(), collectionName);
867 
868                 object removedAtDate = v["removed_at_date"];
869                 if (removedAtDate != null && parameters.Contains(k))
870                     Deprecate(String.Format("Param '{0}' is deprecated. See the module docs for more information", k),
871                               (DateTime)removedAtDate, collectionName);
872             }
873         }
874 
CheckArguments(IDictionary spec, IDictionary param, List<string> legalInputs)875         private void CheckArguments(IDictionary spec, IDictionary param, List<string> legalInputs)
876         {
877             // initially parse the params and check for unsupported ones and set internal vars
878             CheckUnsupportedArguments(param, legalInputs);
879 
880             // Only run this check if we are at the root argument (optionsContext.Count == 0)
881             if (CheckMode && !(bool)spec["supports_check_mode"] && optionsContext.Count == 0)
882             {
883                 Result["skipped"] = true;
884                 Result["msg"] = String.Format("remote module ({0}) does not support check mode", ModuleName);
885                 ExitJson();
886             }
887             IDictionary optionSpec = (IDictionary)spec["options"];
888 
889             CheckMutuallyExclusive(param, (IList)spec["mutually_exclusive"]);
890             CheckRequiredArguments(optionSpec, param);
891 
892             // set the parameter types based on the type spec value
893             foreach (DictionaryEntry entry in optionSpec)
894             {
895                 string k = (string)entry.Key;
896                 Hashtable v = (Hashtable)entry.Value;
897 
898                 object value = param.Contains(k) ? param[k] : null;
899                 if (value != null)
900                 {
901                     // convert the current value to the wanted type
902                     Delegate typeConverter;
903                     string type;
904                     if (v["type"].GetType() == typeof(string))
905                     {
906                         type = (string)v["type"];
907                         typeConverter = optionTypes[type];
908                     }
909                     else
910                     {
911                         type = "delegate";
912                         typeConverter = (Delegate)v["type"];
913                     }
914 
915                     try
916                     {
917                         value = typeConverter.DynamicInvoke(value);
918                         param[k] = value;
919                     }
920                     catch (Exception e)
921                     {
922                         string msg = String.Format("argument for {0} is of type {1} and we were unable to convert to {2}: {3}",
923                             k, value.GetType(), type, e.InnerException.Message);
924                         FailJson(FormatOptionsContext(msg));
925                     }
926 
927                     // ensure it matches the choices if there are choices set
928                     List<string> choices = ((List<object>)v["choices"]).Select(x => x.ToString()).Cast<string>().ToList();
929                     if (choices.Count > 0)
930                     {
931                         List<string> values;
932                         string choiceMsg;
933                         if (type == "list")
934                         {
935                             values = ((List<object>)value).Select(x => x.ToString()).Cast<string>().ToList();
936                             choiceMsg = "one or more of";
937                         }
938                         else
939                         {
940                             values = new List<string>() { value.ToString() };
941                             choiceMsg = "one of";
942                         }
943 
944                         List<string> diffList = values.Except(choices, StringComparer.OrdinalIgnoreCase).ToList();
945                         List<string> caseDiffList = values.Except(choices).ToList();
946                         if (diffList.Count > 0)
947                         {
948                             string msg = String.Format("value of {0} must be {1}: {2}. Got no match for: {3}",
949                                                        k, choiceMsg, String.Join(", ", choices), String.Join(", ", diffList));
950                             FailJson(FormatOptionsContext(msg));
951                         }
952                         /*
953                         For now we will just silently accept case insensitive choices, uncomment this if we want to add it back in
954                         else if (caseDiffList.Count > 0)
955                         {
956                             // For backwards compatibility with Legacy.psm1 we need to be matching choices that are not case sensitive.
957                             // We will warn the user it was case insensitive and tell them this will become case sensitive in the future.
958                             string msg = String.Format(
959                                 "value of {0} was a case insensitive match of {1}: {2}. Checking of choices will be case sensitive in a future Ansible release. Case insensitive matches were: {3}",
960                                 k, choiceMsg, String.Join(", ", choices), String.Join(", ", caseDiffList.Select(x => RemoveNoLogValues(x, noLogValues)))
961                             );
962                             Warn(FormatOptionsContext(msg));
963                         }*/
964                     }
965                 }
966             }
967 
968             CheckRequiredTogether(param, (IList)spec["required_together"]);
969             CheckRequiredOneOf(param, (IList)spec["required_one_of"]);
970             CheckRequiredIf(param, (IList)spec["required_if"]);
971             CheckRequiredBy(param, (IDictionary)spec["required_by"]);
972 
973             // finally ensure all missing parameters are set to null and handle sub options
974             foreach (DictionaryEntry entry in optionSpec)
975             {
976                 string k = (string)entry.Key;
977                 IDictionary v = (IDictionary)entry.Value;
978 
979                 if (!param.Contains(k))
980                     param[k] = null;
981 
982                 CheckSubOption(param, k, v);
983             }
984         }
985 
CheckUnsupportedArguments(IDictionary param, List<string> legalInputs)986         private void CheckUnsupportedArguments(IDictionary param, List<string> legalInputs)
987         {
988             HashSet<string> unsupportedParameters = new HashSet<string>();
989             HashSet<string> caseUnsupportedParameters = new HashSet<string>();
990             List<string> removedParameters = new List<string>();
991 
992             foreach (DictionaryEntry entry in param)
993             {
994                 string paramKey = (string)entry.Key;
995                 if (!legalInputs.Contains(paramKey, StringComparer.OrdinalIgnoreCase))
996                     unsupportedParameters.Add(paramKey);
997                 else if (!legalInputs.Contains(paramKey))
998                     // For backwards compatibility we do not care about the case but we need to warn the users as this will
999                     // change in a future Ansible release.
1000                     caseUnsupportedParameters.Add(paramKey);
1001                 else if (paramKey.StartsWith("_ansible_"))
1002                 {
1003                     removedParameters.Add(paramKey);
1004                     string key = paramKey.Replace("_ansible_", "");
1005                     // skip setting NoLog if NoLog is already set to true (set by the module)
1006                     // or there's no mapping for this key
1007                     if ((key == "no_log" && NoLog == true) || (passVars[key] == null))
1008                         continue;
1009 
1010                     object value = entry.Value;
1011                     if (passBools.Contains(key))
1012                         value = ParseBool(value);
1013                     else if (passInts.Contains(key))
1014                         value = ParseInt(value);
1015 
1016                     string propertyName = passVars[key];
1017                     PropertyInfo property = typeof(AnsibleModule).GetProperty(propertyName);
1018                     FieldInfo field = typeof(AnsibleModule).GetField(propertyName, BindingFlags.NonPublic | BindingFlags.Instance);
1019                     if (property != null)
1020                         property.SetValue(this, value, null);
1021                     else if (field != null)
1022                         field.SetValue(this, value);
1023                     else
1024                         FailJson(String.Format("implementation error: unknown AnsibleModule property {0}", propertyName));
1025                 }
1026             }
1027             foreach (string parameter in removedParameters)
1028                 param.Remove(parameter);
1029 
1030             if (unsupportedParameters.Count > 0)
1031             {
1032                 legalInputs.RemoveAll(x => passVars.Keys.Contains(x.Replace("_ansible_", "")));
1033                 string msg = String.Format("Unsupported parameters for ({0}) module: {1}", ModuleName, String.Join(", ", unsupportedParameters));
1034                 msg = String.Format("{0}. Supported parameters include: {1}", FormatOptionsContext(msg), String.Join(", ", legalInputs));
1035                 FailJson(msg);
1036             }
1037 
1038             /*
1039             // Uncomment when we want to start warning users around options that are not a case sensitive match to the spec
1040             if (caseUnsupportedParameters.Count > 0)
1041             {
1042                 legalInputs.RemoveAll(x => passVars.Keys.Contains(x.Replace("_ansible_", "")));
1043                 string msg = String.Format("Parameters for ({0}) was a case insensitive match: {1}", ModuleName, String.Join(", ", caseUnsupportedParameters));
1044                 msg = String.Format("{0}. Module options will become case sensitive in a future Ansible release. Supported parameters include: {1}",
1045                     FormatOptionsContext(msg), String.Join(", ", legalInputs));
1046                 Warn(msg);
1047             }*/
1048 
1049             // Make sure we convert all the incorrect case params to the ones set by the module spec
1050             foreach (string key in caseUnsupportedParameters)
1051             {
1052                 string correctKey = legalInputs[legalInputs.FindIndex(s => s.Equals(key, StringComparison.OrdinalIgnoreCase))];
1053                 object value = param[key];
1054                 param.Remove(key);
1055                 param.Add(correctKey, value);
1056             }
1057         }
1058 
CheckMutuallyExclusive(IDictionary param, IList mutuallyExclusive)1059         private void CheckMutuallyExclusive(IDictionary param, IList mutuallyExclusive)
1060         {
1061             if (mutuallyExclusive == null)
1062                 return;
1063 
1064             foreach (object check in mutuallyExclusive)
1065             {
1066                 List<string> mutualCheck = ((IList)check).Cast<string>().ToList();
1067                 int count = 0;
1068                 foreach (string entry in mutualCheck)
1069                     if (param.Contains(entry))
1070                         count++;
1071 
1072                 if (count > 1)
1073                 {
1074                     string msg = String.Format("parameters are mutually exclusive: {0}", String.Join(", ", mutualCheck));
1075                     FailJson(FormatOptionsContext(msg));
1076                 }
1077             }
1078         }
1079 
CheckRequiredArguments(IDictionary spec, IDictionary param)1080         private void CheckRequiredArguments(IDictionary spec, IDictionary param)
1081         {
1082             List<string> missing = new List<string>();
1083             foreach (DictionaryEntry entry in spec)
1084             {
1085                 string k = (string)entry.Key;
1086                 Hashtable v = (Hashtable)entry.Value;
1087 
1088                 // set defaults for values not already set
1089                 object defaultValue = v["default"];
1090                 if (defaultValue != null && !param.Contains(k))
1091                     param[k] = defaultValue;
1092 
1093                 // check required arguments
1094                 bool required = (bool)v["required"];
1095                 if (required && !param.Contains(k))
1096                     missing.Add(k);
1097             }
1098             if (missing.Count > 0)
1099             {
1100                 string msg = String.Format("missing required arguments: {0}", String.Join(", ", missing));
1101                 FailJson(FormatOptionsContext(msg));
1102             }
1103         }
1104 
CheckRequiredTogether(IDictionary param, IList requiredTogether)1105         private void CheckRequiredTogether(IDictionary param, IList requiredTogether)
1106         {
1107             if (requiredTogether == null)
1108                 return;
1109 
1110             foreach (object check in requiredTogether)
1111             {
1112                 List<string> requiredCheck = ((IList)check).Cast<string>().ToList();
1113                 List<bool> found = new List<bool>();
1114                 foreach (string field in requiredCheck)
1115                     if (param.Contains(field))
1116                         found.Add(true);
1117                     else
1118                         found.Add(false);
1119 
1120                 if (found.Contains(true) && found.Contains(false))
1121                 {
1122                     string msg = String.Format("parameters are required together: {0}", String.Join(", ", requiredCheck));
1123                     FailJson(FormatOptionsContext(msg));
1124                 }
1125             }
1126         }
1127 
CheckRequiredOneOf(IDictionary param, IList requiredOneOf)1128         private void CheckRequiredOneOf(IDictionary param, IList requiredOneOf)
1129         {
1130             if (requiredOneOf == null)
1131                 return;
1132 
1133             foreach (object check in requiredOneOf)
1134             {
1135                 List<string> requiredCheck = ((IList)check).Cast<string>().ToList();
1136                 int count = 0;
1137                 foreach (string field in requiredCheck)
1138                     if (param.Contains(field))
1139                         count++;
1140 
1141                 if (count == 0)
1142                 {
1143                     string msg = String.Format("one of the following is required: {0}", String.Join(", ", requiredCheck));
1144                     FailJson(FormatOptionsContext(msg));
1145                 }
1146             }
1147         }
1148 
CheckRequiredIf(IDictionary param, IList requiredIf)1149         private void CheckRequiredIf(IDictionary param, IList requiredIf)
1150         {
1151             if (requiredIf == null)
1152                 return;
1153 
1154             foreach (object check in requiredIf)
1155             {
1156                 IList requiredCheck = (IList)check;
1157                 List<string> missing = new List<string>();
1158                 List<string> missingFields = new List<string>();
1159                 int maxMissingCount = 1;
1160                 bool oneRequired = false;
1161 
1162                 if (requiredCheck.Count < 3 && requiredCheck.Count < 4)
1163                     FailJson(String.Format("internal error: invalid required_if value count of {0}, expecting 3 or 4 entries", requiredCheck.Count));
1164                 else if (requiredCheck.Count == 4)
1165                     oneRequired = (bool)requiredCheck[3];
1166 
1167                 string key = (string)requiredCheck[0];
1168                 object val = requiredCheck[1];
1169                 IList requirements = (IList)requiredCheck[2];
1170 
1171                 if (ParseStr(param[key]) != ParseStr(val))
1172                     continue;
1173 
1174                 string term = "all";
1175                 if (oneRequired)
1176                 {
1177                     maxMissingCount = requirements.Count;
1178                     term = "any";
1179                 }
1180 
1181                 foreach (string required in requirements.Cast<string>())
1182                     if (!param.Contains(required))
1183                         missing.Add(required);
1184 
1185                 if (missing.Count >= maxMissingCount)
1186                 {
1187                     string msg = String.Format("{0} is {1} but {2} of the following are missing: {3}",
1188                         key, val.ToString(), term, String.Join(", ", missing));
1189                     FailJson(FormatOptionsContext(msg));
1190                 }
1191             }
1192         }
1193 
CheckRequiredBy(IDictionary param, IDictionary requiredBy)1194         private void CheckRequiredBy(IDictionary param, IDictionary requiredBy)
1195         {
1196             foreach (DictionaryEntry entry in requiredBy)
1197             {
1198                 string key = (string)entry.Key;
1199                 if (!param.Contains(key))
1200                     continue;
1201 
1202                 List<string> missing = new List<string>();
1203                 List<string> requires = ParseList(entry.Value).Cast<string>().ToList();
1204                 foreach (string required in requires)
1205                     if (!param.Contains(required))
1206                         missing.Add(required);
1207 
1208                 if (missing.Count > 0)
1209                 {
1210                     string msg = String.Format("missing parameter(s) required by '{0}': {1}", key, String.Join(", ", missing));
1211                     FailJson(FormatOptionsContext(msg));
1212                 }
1213             }
1214         }
1215 
CheckSubOption(IDictionary param, string key, IDictionary spec)1216         private void CheckSubOption(IDictionary param, string key, IDictionary spec)
1217         {
1218             object value = param[key];
1219 
1220             string type;
1221             if (spec["type"].GetType() == typeof(string))
1222                 type = (string)spec["type"];
1223             else
1224                 type = "delegate";
1225 
1226             string elements = null;
1227             Delegate typeConverter = null;
1228             if (spec["elements"] != null && spec["elements"].GetType() == typeof(string))
1229             {
1230                 elements = (string)spec["elements"];
1231                 typeConverter = optionTypes[elements];
1232             }
1233             else if (spec["elements"] != null)
1234             {
1235                 elements = "delegate";
1236                 typeConverter = (Delegate)spec["elements"];
1237             }
1238 
1239             if (!(type == "dict" || (type == "list" && elements != null)))
1240                 // either not a dict, or list with the elements set, so continue
1241                 return;
1242             else if (type == "list")
1243             {
1244                 // cast each list element to the type specified
1245                 if (value == null)
1246                     return;
1247 
1248                 List<object> newValue = new List<object>();
1249                 foreach (object element in (List<object>)value)
1250                 {
1251                     if (elements == "dict")
1252                         newValue.Add(ParseSubSpec(spec, element, key));
1253                     else
1254                     {
1255                         try
1256                         {
1257                             object newElement = typeConverter.DynamicInvoke(element);
1258                             newValue.Add(newElement);
1259                         }
1260                         catch (Exception e)
1261                         {
1262                             string msg = String.Format("argument for list entry {0} is of type {1} and we were unable to convert to {2}: {3}",
1263                                 key, element.GetType(), elements, e.Message);
1264                             FailJson(FormatOptionsContext(msg));
1265                         }
1266                     }
1267                 }
1268 
1269                 param[key] = newValue;
1270             }
1271             else
1272                 param[key] = ParseSubSpec(spec, value, key);
1273         }
1274 
ParseSubSpec(IDictionary spec, object value, string context)1275         private object ParseSubSpec(IDictionary spec, object value, string context)
1276         {
1277             bool applyDefaults = (bool)spec["apply_defaults"];
1278 
1279             // set entry to an empty dict if apply_defaults is set
1280             IDictionary optionsSpec = (IDictionary)spec["options"];
1281             if (applyDefaults && optionsSpec.Keys.Count > 0 && value == null)
1282                 value = new Dictionary<string, object>();
1283             else if (optionsSpec.Keys.Count == 0 || value == null)
1284                 return value;
1285 
1286             optionsContext.Add(context);
1287             Dictionary<string, object> newValue = (Dictionary<string, object>)ParseDict(value);
1288             Dictionary<string, string> aliases = GetAliases(spec, newValue);
1289             SetNoLogValues(spec, newValue);
1290 
1291             List<string> subLegalInputs = optionsSpec.Keys.Cast<string>().ToList();
1292             subLegalInputs.AddRange(aliases.Keys.Cast<string>().ToList());
1293 
1294             CheckArguments(spec, newValue, subLegalInputs);
1295             optionsContext.RemoveAt(optionsContext.Count - 1);
1296             return newValue;
1297         }
1298 
GetFormattedResults(Dictionary<string, object> result)1299         private string GetFormattedResults(Dictionary<string, object> result)
1300         {
1301             if (!result.ContainsKey("invocation"))
1302                 result["invocation"] = new Dictionary<string, object>() { { "module_args", RemoveNoLogValues(Params, noLogValues) } };
1303 
1304             if (warnings.Count > 0)
1305                 result["warnings"] = warnings;
1306 
1307             if (deprecations.Count > 0)
1308                 result["deprecations"] = deprecations;
1309 
1310             if (Diff.Count > 0 && DiffMode)
1311                 result["diff"] = Diff;
1312 
1313             return ToJson(result);
1314         }
1315 
FormatLogData(object data, int indentLevel)1316         private string FormatLogData(object data, int indentLevel)
1317         {
1318             if (data == null)
1319                 return "$null";
1320 
1321             string msg = "";
1322             if (data is IList)
1323             {
1324                 string newMsg = "";
1325                 foreach (object value in (IList)data)
1326                 {
1327                     string entryValue = FormatLogData(value, indentLevel + 2);
1328                     newMsg += String.Format("\r\n{0}- {1}", new String(' ', indentLevel), entryValue);
1329                 }
1330                 msg += newMsg;
1331             }
1332             else if (data is IDictionary)
1333             {
1334                 bool start = true;
1335                 foreach (DictionaryEntry entry in (IDictionary)data)
1336                 {
1337                     string newMsg = FormatLogData(entry.Value, indentLevel + 2);
1338                     if (!start)
1339                         msg += String.Format("\r\n{0}", new String(' ', indentLevel));
1340                     msg += String.Format("{0}: {1}", (string)entry.Key, newMsg);
1341                     start = false;
1342                 }
1343             }
1344             else
1345                 msg = (string)RemoveNoLogValues(ParseStr(data), noLogValues);
1346 
1347             return msg;
1348         }
1349 
RemoveNoLogValues(object value, HashSet<string> noLogStrings)1350         private object RemoveNoLogValues(object value, HashSet<string> noLogStrings)
1351         {
1352             Queue<Tuple<object, object>> deferredRemovals = new Queue<Tuple<object, object>>();
1353             object newValue = RemoveValueConditions(value, noLogStrings, deferredRemovals);
1354 
1355             while (deferredRemovals.Count > 0)
1356             {
1357                 Tuple<object, object> data = deferredRemovals.Dequeue();
1358                 object oldData = data.Item1;
1359                 object newData = data.Item2;
1360 
1361                 if (oldData is IDictionary)
1362                 {
1363                     foreach (DictionaryEntry entry in (IDictionary)oldData)
1364                     {
1365                         object newElement = RemoveValueConditions(entry.Value, noLogStrings, deferredRemovals);
1366                         ((IDictionary)newData).Add((string)entry.Key, newElement);
1367                     }
1368                 }
1369                 else
1370                 {
1371                     foreach (object element in (IList)oldData)
1372                     {
1373                         object newElement = RemoveValueConditions(element, noLogStrings, deferredRemovals);
1374                         ((IList)newData).Add(newElement);
1375                     }
1376                 }
1377             }
1378 
1379             return newValue;
1380         }
1381 
RemoveValueConditions(object value, HashSet<string> noLogStrings, Queue<Tuple<object, object>> deferredRemovals)1382         private object RemoveValueConditions(object value, HashSet<string> noLogStrings, Queue<Tuple<object, object>> deferredRemovals)
1383         {
1384             if (value == null)
1385                 return value;
1386 
1387             Type valueType = value.GetType();
1388             HashSet<Type> numericTypes = new HashSet<Type>
1389             {
1390                 typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint),
1391                 typeof(long), typeof(ulong), typeof(decimal), typeof(double), typeof(float)
1392             };
1393 
1394             if (numericTypes.Contains(valueType) || valueType == typeof(bool))
1395             {
1396                 string valueString = ParseStr(value);
1397                 if (noLogStrings.Contains(valueString))
1398                     return "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER";
1399                 foreach (string omitMe in noLogStrings)
1400                     if (valueString.Contains(omitMe))
1401                         return "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER";
1402             }
1403             else if (valueType == typeof(DateTime))
1404                 value = ((DateTime)value).ToString("o");
1405             else if (value is IList)
1406             {
1407                 List<object> newValue = new List<object>();
1408                 deferredRemovals.Enqueue(new Tuple<object, object>((IList)value, newValue));
1409                 value = newValue;
1410             }
1411             else if (value is IDictionary)
1412             {
1413                 Hashtable newValue = new Hashtable();
1414                 deferredRemovals.Enqueue(new Tuple<object, object>((IDictionary)value, newValue));
1415                 value = newValue;
1416             }
1417             else
1418             {
1419                 string stringValue = value.ToString();
1420                 if (noLogStrings.Contains(stringValue))
1421                     return "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER";
1422                 foreach (string omitMe in noLogStrings)
1423                     if (stringValue.Contains(omitMe))
1424                         return (stringValue).Replace(omitMe, "********");
1425                 value = stringValue;
1426             }
1427             return value;
1428         }
1429 
CleanupFiles(object s, EventArgs ev)1430         private void CleanupFiles(object s, EventArgs ev)
1431         {
1432             foreach (string path in cleanupFiles)
1433             {
1434                 if (File.Exists(path))
1435                     File.Delete(path);
1436                 else if (Directory.Exists(path))
1437                     Directory.Delete(path, true);
1438             }
1439             cleanupFiles = new List<string>();
1440         }
1441 
FormatOptionsContext(string msg, string prefix = R)1442         private string FormatOptionsContext(string msg, string prefix = " ")
1443         {
1444             if (optionsContext.Count > 0)
1445                 msg += String.Format("{0}found in {1}", prefix, String.Join(" -> ", optionsContext));
1446             return msg;
1447         }
1448 
1449         [DllImport("kernel32.dll")]
GetConsoleWindow()1450         private static extern IntPtr GetConsoleWindow();
1451 
ExitModule(int rc)1452         private static void ExitModule(int rc)
1453         {
1454             // When running in a Runspace Environment.Exit will kill the entire
1455             // process which is not what we want, detect if we are in a
1456             // Runspace and call a ScriptBlock with exit instead.
1457             if (Runspace.DefaultRunspace != null)
1458                 ScriptBlock.Create("Set-Variable -Name LASTEXITCODE -Value $args[0] -Scope Global; exit $args[0]").Invoke(rc);
1459             else
1460             {
1461                 // Used for local debugging in Visual Studio
1462                 if (System.Diagnostics.Debugger.IsAttached)
1463                 {
1464                     Console.WriteLine("Press enter to continue...");
1465                     Console.ReadLine();
1466                 }
1467                 Environment.Exit(rc);
1468             }
1469         }
1470 
WriteLineModule(string line)1471         private static void WriteLineModule(string line)
1472         {
1473             Console.WriteLine(line);
1474         }
1475     }
1476 }
1477