Configuring iDRAC using RedFish API via Ansible.

Having a large number of iDRAC out of band (OOB) interfaces to configure on Dell hardware can seem like a mind numbing daunting task to administrators everywhere. Recently my team was tasked with the following basic tasks on around 80 iDRACs:

  • Hostname
  • DNS Setting
  • Syslog Settings
  • LDAP configuration & RBAC mapping
  • SSL Certificate update (interaction with a Microsoft PKI server (certsrv)
  • NTP configuration
  • Update root credential and place into Hashicorp Vault

After a little research and fiddling with APIs / RACADM and the GUI I settled upon using an Ansible module ‘idrac_redfish_config

Without question its the easiest way to configure the bulk of components within iDRAC. ** Certain elements are not possible with this module – I will cover this later **

To explain how to actually use the module, take this task as an example:

- name: Configure NTP and Timezone on iDRAC
  idrac_redfish_config:
    category: Manager
    command: SetManagerAttributes
    manager_attributes:
      NTPConfigGroup.1.NTPEnable: "Enabled"
      NTPConfigGroup.1.NTP1: "{{ ntp_server }}"
      Time.1.Timezone: "{{ timezone }}"
    baseuri: "{{ idrac_ip }}"
    username: "{{ default_user }}"
    password: "{{ idrac_pass }}" 

The manager_attributes section is the key to this module, and typically it is poorly described within the Ansible documentation. The best way to obtain these attributes is to open an iDRAC GUI up, hit F12 to developer mode and track the network activity when saving a change. An attributes POST action includes the payload in the correct format for inclusion in this Ansible task.

My basic playbook for configuration is as follows (excluding certificate change).

---
- name: Configure NTP and Timezone on iDRAC
  idrac_redfish_config:
    category: Manager
    command: SetManagerAttributes
    manager_attributes:
      NTPConfigGroup.1.NTPEnable: "Enabled"
      NTPConfigGroup.1.NTP1: "{{ ntp_server }}"
      Time.1.Timezone: "{{ timezone }}"
    baseuri: "{{ idrac_ip }}"
    username: "{{ default_user }}"
    password: "{{ idrac_pass }}" 


- name: Configure DNS Servers
  idrac_redfish_config:
    category: Manager
    command: SetManagerAttributes
    manager_attributes:
      IPv4Static.1.DNS1: "{{ dns_server.primary }}"
      IPv4Static.1.DNS2: "{{ dns_server.secondary }}"
    baseuri: "{{ idrac_ip }}"
    username: "{{ default_user }}"
    password: "{{ idrac_pass }}" 

- name: Configure iDRAC Name and domain 
  idrac_redfish_config:
    category: Manager
    command: SetManagerAttributes
    manager_attributes:
      NIC.1.DNSDomainName: "{{ domain_name }}"
      NIC.1.DNSRacName: "{{ idrac_hostname }}"
      NIC.1.DNSRegister: "Enabled"
    baseuri: "{{ idrac_ip }}"
    username: "{{ default_user }}"
    password: "{{ idrac_pass }}" 

- name: Configure TLS 1.2 
  idrac_redfish_config:
    category: Manager
    command: SetManagerAttributes
    manager_attributes:
      WebServer.1.TLSProtocol: "TLS 1.2 Only"
      NIC.1.DNSRacName: "{{ idrac_hostname }}"
      NIC.1.DNSRegister: "Enabled"
    baseuri: "{{ idrac_ip }}"
    username: "{{ default_user }}"
    password: "{{ idrac_pass }}" 

- name: Configure LDAPS
  idrac_redfish_config:
    category: Manager
    command: SetManagerAttributes
    manager_attributes:
      LDAP.1.Enable: "Enabled"
      LDAPRoleGroup.1.DN: "CN=DG_IDRAC_ADMINS,OU=1_IDRAC,OU=blah,DC=blah"
      LDAPRoleGroup.1.Privilege: 511
      LDAPRoleGroup.2.DN: "CN=DG_IDRAC_READ,OU=1_IDRAC,OU=blah,DC=blah"
      LDAPRoleGroup.2.Privilege: 1
      LDAP.1.BaseDN: "{{ base_dn }}"
      LDAP.1.BindDN: "{{ bind_account_name }},{{ base_dn }}"
      LDAP.1.BindPassword: "{{ ldap_bind_pwd }}"
      LDAP.1.CertValidationEnable: "Disabled"
      LDAP.1.Enable: "Enabled"
      LDAP.1.GroupAttribute: ""
      LDAP.1.GroupAttributeIsDN: "Enabled"
      LDAP.1.Port: 636
      LDAP.1.SearchFilter: ""
      LDAP.1.Server: "{{ domain_name }}"
      LDAP.1.UserAttribute: "sAMAccountName"
    baseuri: "{{ idrac_ip }}"
    username: "{{ default_user }}"
    password: "{{ idrac_pass }}" 

- name: Configure Syslog
  idrac_redfish_config:
    category: Manager
    command: SetManagerAttributes
    manager_attributes:
      SysLog.1.Port: 514
      SysLog.1.Server1: 1.2.3.4
      SysLog.1.SysLogEnable: "Enabled"
    baseuri: "{{ idrac_ip }}"
    username: "{{ default_user }}"
    password: "{{ idrac_pass }}" 

- name: Login to Vault (Root NS) as Service Account 
  uri:
    url: "{{ vault_url }}/v1/auth/ldap/login/{{ vault_svc_acc_username }}"
    method: POST
    body_format: json
    body: {"password":"{{ vault_svc_acc_password }}"}
    return_content: yes
    follow_redirects: all
    status_code: 200
    validate_certs: no
  register: data
  no_log: true

- name: Fetch a new password from Vault
  uri:
    url: "{{ vault_url }}/v1/gen/password"
    method: POST
    body_format: json
    headers:
        X-Vault-Token: "{{ data.json.auth.client_token }}"
    body: {"length":"15", "digits":"3", "symbols":"3", "allow_uppercase":"true"}
    return_content: yes
    status_code: 200
    validate_certs: no
  register: new_pass
  no_log: true

- name: Login to Vault as Service Account
  uri:
    url: "{{ vault_url }}/v1/auth/ldap/login/{{ vault_svc_acc_username }}"
    method: POST
    body_format: json
    headers:
        X-Vault-Namespace: "{{ vault_namespace }}"
    body: {"password":"{{ vault_svc_acc_password }}"}
    return_content: yes
    status_code: 200
    validate_certs: no
  register: ns_data
  no_log: true

- name: Post new server record to Vault - iDRAC
  uri:
    url: "{{ vault_url }}/v1/data/servers/idrac/{{ idrac_hostname | lower}}/root"
    body_format: json
    method: POST
    headers:
        X-Vault-Token: "{{ ns_data.json.auth.client_token }}"
        X-Vault-Namespace: "{{ vault_namespace }}"
    body: { "options": { "max_versions": "12" }, "data": { "password":"{{ new_pass.json.data.value }}" }}
    return_content: yes
    status_code: 200
    validate_certs: no
  no_log: true

- name: Change Root Password
  idrac_redfish_config:
    category: Manager
    command: SetManagerAttributes
    manager_attributes:
      Users.2.AuthenticationProtocol: "SHA"
      Users.2.EmailAddress: ""
      Users.2.Enable: "Enabled"
      Users.2.IpmiLanPrivilege: "Administrator"
      Users.2.IpmiSerialPrivilege: "Administrator"
      Users.2.Password: "{{ new_pass.json.data.value }}"
      Users.2.PrivacyProtocol: "AES"
      Users.2.Privilege: 511
      Users.2.ProtocolEnable: "Disabled"
      Users.2.Simple2FA: "Disabled"
      Users.2.SolEnable: "Enabled"
      Users.2.UseEmail: "Disabled"
      Users.2.UserName: "root"
    baseuri: "{{ idrac_ip }}"
    username: "{{ default_user }}"
    password: "{{ idrac_pass }}" 

Certificates…

Of course changing the SSL certificate wouldn’t be as easy as all the elements above. The Ansible module does not support the changing of the certificate for some unknown reason, so I had to delve into the hell which is the Dell developer website, specifically for these poorly described API calls….

https://developer.dell.com/apis/2978/versions/5.xx/openapi.yaml/paths/~1redfish~1v1~1CertificateService~1Actions~1CertificateService.GenerateCSR/post

https://developer.dell.com/apis/2978/versions/5.xx/openapi.yaml/paths/~1redfish~1v1~1CertificateService~1Actions~1CertificateService.ReplaceCertificate/post

The flow for this playbook is as follows:

  • Generate CSR on the iDRAC and export
  • Send CSR to Microsoft PKI server and request against a specific template
  • Obtain certificate for certsrv
  • Parse cert and upload to iDRAC
  • Reset iDRAC for cert to take effect
--
- name: Generate iDRAC CSR 
  uri:
    url: "https://{{ idrac_ip }}/redfish/v1/CertificateService/Actions/CertificateService.GenerateCSR"
    method: POST
    url_username: "{{ default_user}}"
    url_password: "{{ idrac_pass }}"
    force_basic_auth: yes
    return_content: yes
    body_format: json
    status_code: 200
    validate_certs: no
    body: '{
      "Country" : "GB",
      "State" : "StateHere",
      "City"  : "Cityhere",
      "Organization" : "Blah",
      "OrganizationalUnit" : "BlahOU",
      "CommonName" : "{{ idrac_hostname }}.{{ domain_name }}",
      "AlternativeNames" : [
        "{{ idrac_hostname }}.{{ domain_name }}",
        "{{ idrac_hostname }}"
      ],
      "CertificateCollection" : {
        "@odata.id" : "/redfish/v1/Managers/iDRAC.Embedded.1/NetworkProtocol/HTTPS/Certificates"
      }
    }'
  no_log: false
  register: data

- name: Set CSR as new fact
  set_fact:
    new_csr: "{{ data.json.CSRString | replace('\n','') }}"

- name: Print out the CSR
  debug:
    var: new_csr

- name: Send CSR to Microsoft PKI server
  uri:
    url: "https://{{ pki_server }}/certsrv/certfnsh.asp"
    method: POST
    url_username: "{{ pki_svc_account }}@{{ domain_name | upper }}"
    url_password: "{{ pki_pwd }}"
    use_gssapi: yes
    return_content: yes
    body_format: form-urlencoded
    status_code: 200
    validate_certs: no
    body: 
      Mode: "newreq"
      CertRequest: "{{ new_csr }}"
      CertAttrib: "CertificateTemplate:{{ pki_template }}"
      FriendlyType: "Saved-Request Certificate"
      TargetStoreFlags: "0"
      SaveCert: "yes"
  no_log: true
  register: cert_out

- name: Set content as a fact
  set_fact:
    content: "{{ cert_out.content }}"

- name: Push new fact to file
  copy:
    content: "{{ content }}"
    dest: ./content

- name: Grab Request ID 
  shell: pwsh -c "./regex_search.ps1"
  register: req_id

- name: print req id
  debug:
    msg: "{{ req_id }}"

- name: Obtain certificate from PKI server
  uri:
    url: "https://{{ pki_server }}/certsrv/certnew.cer?ReqID={{ req_id.stdout }}&Enc=b64"
    method: GET
    url_username: "{{ pki_svc_account }}@{{ domain_name | upper }}"
    url_password: "{{ pki_pwd }}"
    use_gssapi: yes
    return_content: yes
    status_code: 200
    validate_certs: no
  no_log: true
  register: new_cert

- name: Parse the new certificate to clear up the rubbish \r instances returned from MS
  set_fact:
    cert: "{{ new_cert.content | regex_replace('\\r?','') }}"

- name: Print the new parsed certificate
  debug:
    var: cert

- name: Upload Certificate to iDRAC
  uri:
    url: "https://{{ idrac_ip }}/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/DelliDRACCardService/Actions/DelliDRACCardService.ImportSSLCertificate"
    method: POST
    url_username: "{{ default_user}}"
    url_password: "{{ idrac_pass }}"
    force_basic_auth: yes
    return_content: yes
    body_format: json
    status_code: 200
    validate_certs: no
    body: '{
      "SSLCertificateFile" : "{{ cert }}",
      "CertificateType" : "Server"
    }'
  no_log: true
  register: data

- name: Reset IDRAC to allow the certificate take effect
  uri:
    url: "https://{{ idrac_ip }}/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Manager.Reset"
    method: POST
    url_username: "{{ default_user}}"
    url_password: "{{ idrac_pass }}"
    force_basic_auth: yes
    return_content: yes
    body_format: json
    status_code: 204
    validate_certs: no
    body: '{
      "ResetType" : "GracefulRestart"
    }'
  no_log: false

- name: "Wait for {{ idrac_ip }} to come up"
  uri:
    url: "https://{{ idrac_ip }}//restgui/start.html"
    validate_certs: no
    method: GET
    follow_redirects: none
  register: _result
  until: _result.status == 200
  retries: 30
  delay: 20
  # 5 minute wait time

For these interested, I have a small PowerShell hack in this code to grab the ReqID from the output from the PKI server which is….

I ran out of time to integrate this into a Ansible regex_search task, but I’m sure its possible!

regex_search.ps1
$reqID = (get-content ./content | select-string -Pattern "ReqID=[0-9]{1,5}" | select-object -Index 0).Matches.Value.Split("=")[1]
write-host $reqID

I hope this helps someone out!!

1 Comment

  1. Florian Permalink

    Hi,
    thank you for your inspirational post. I have found your post for obtaining Certificates through Microsoft PKI with Ansible. Here the Task which works for us with Regex within Ansible and without Powershell:

    – name: “Find Request ID”
    ansible.builtin.set_fact:
    cert_reqid: “{{ cert_out.content | regex_search(‘ReqID=[0-9]{1,6}’) }}”

    Our RegID is 6 digits instead 5. Maybe in future 7 digits. But that´s next construction site.

    Reply

Leave a Reply