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