github.com/SUSE/skuba@v1.4.17/skuba-update/test/unit/skuba_update_test.py (about)

     1  #!/usr/bin/env python
     2  # -*- encoding: utf-8 -*-
     3  
     4  # Copyright (c) 2019 SUSE LLC.
     5  #
     6  # Licensed under the Apache License, Version 2.0 (the "License");
     7  # you may not use this file except in compliance with the License.
     8  # You may obtain a copy of the License at
     9  #
    10  #     http://www.apache.org/licenses/LICENSE-2.0
    11  #
    12  # Unless required by applicable law or agreed to in writing, software
    13  # distributed under the License is distributed on an "AS IS" BASIS,
    14  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15  # See the License for the specific language governing permissions and
    16  # limitations under the License.
    17  
    18  import json
    19  from collections import namedtuple
    20  
    21  from mock import patch, call, mock_open, Mock, ANY
    22  from skuba_update.skuba_update import (
    23      main,
    24      update,
    25      run_command,
    26      run_zypper_command,
    27      node_name_from_machine_id,
    28      annotate,
    29      is_reboot_needed,
    30      reboot_sentinel_file,
    31      annotate_updates_available,
    32      annotate_caasp_release_version,
    33      get_update_list,
    34      restart_services,
    35      REBOOT_REQUIRED_PATH,
    36      ZYPPER_EXIT_INF_UPDATE_NEEDED,
    37      ZYPPER_EXIT_INF_RESTART_NEEDED,
    38      ZYPPER_EXIT_INF_REBOOT_NEEDED,
    39      KUBE_UPDATES_KEY,
    40      KUBE_SECURITY_UPDATES_KEY,
    41      KUBE_DISRUPTIVE_UPDATES_KEY,
    42      KUBE_CAASP_RELEASE_VERSION_KEY
    43  )
    44  
    45  
    46  @patch('subprocess.Popen')
    47  def test_run_command(mock_subprocess):
    48      mock_process = Mock()
    49      mock_process.communicate.return_value = (b'stdout', b'stderr')
    50      mock_process.returncode = 0
    51      mock_subprocess.return_value = mock_process
    52      result = run_command(['/bin/dummycmd', 'arg1'])
    53      assert result.output == "stdout"
    54      assert result.returncode == 0
    55      assert result.error == 'stderr'
    56  
    57      mock_process.returncode = 1
    58      result = run_command(['/bin/dummycmd', 'arg1'])
    59      assert result.output == "stdout"
    60      assert result.returncode == 1
    61  
    62      mock_process.communicate.return_value = (b'', b'stderr')
    63      result = run_command(['/bin/dummycmd', 'arg1'])
    64      assert result.output == ""
    65      assert result.returncode == 1
    66  
    67  
    68  @patch('argparse.ArgumentParser.parse_args')
    69  @patch('subprocess.Popen')
    70  def test_main_wrong_version(mock_subprocess, mock_args):
    71      mock_process = Mock()
    72      mock_process.communicate.return_value = (b'zypper 1.13.0', b'stderr')
    73      mock_process.returncode = 0
    74      mock_subprocess.return_value = mock_process
    75      exception = False
    76      try:
    77          main()
    78      except Exception as e:
    79          exception = True
    80          assert 'higher is required' in str(e)
    81      assert exception
    82  
    83  
    84  @patch('argparse.ArgumentParser.parse_args')
    85  @patch('subprocess.Popen')
    86  def test_main_bad_format_version(mock_subprocess, mock_args):
    87      mock_process = Mock()
    88      mock_process.communicate.return_value = (b'zypper', b'stderr')
    89      mock_process.returncode = 0
    90      mock_subprocess.return_value = mock_process
    91      exception = False
    92      try:
    93          main()
    94      except Exception as e:
    95          exception = True
    96          assert 'Could not parse' in str(e)
    97      assert exception
    98  
    99  
   100  @patch('argparse.ArgumentParser.parse_args')
   101  @patch('subprocess.Popen')
   102  def test_main_no_root(mock_subprocess, mock_args):
   103      mock_process = Mock()
   104      mock_process.communicate.return_value = (b'zypper 1.14.15', b'stderr')
   105      mock_process.returncode = 0
   106      mock_subprocess.return_value = mock_process
   107      exception = False
   108      try:
   109          main()
   110      except Exception as e:
   111          exception = True
   112          assert 'root privileges' in str(e)
   113      assert exception
   114  
   115  
   116  @patch('skuba_update.skuba_update.node_name_from_machine_id')
   117  @patch('skuba_update.skuba_update.annotate_caasp_release_version')
   118  @patch('skuba_update.skuba_update.annotate_updates_available')
   119  @patch('argparse.ArgumentParser.parse_args')
   120  @patch('os.environ.get', new={}.get, spec_set=True)
   121  @patch('os.geteuid')
   122  @patch('subprocess.Popen')
   123  def test_main(
   124      mock_subprocess, mock_geteuid, mock_args,
   125      mock_annotate, mock_annotate_version, mock_name
   126  ):
   127      return_values = [
   128          (b'some_service1\nsome_service2', b''),
   129          (b'zypper 1.14.15', b'')
   130      ]
   131  
   132      def mock_communicate():
   133          if len(return_values) > 1:
   134              return return_values.pop()
   135          else:
   136              return return_values[0]
   137  
   138      args = Mock()
   139      args.annotate_only = False
   140      mock_args.return_value = args
   141      mock_geteuid.return_value = 0
   142      mock_process = Mock()
   143      mock_process.communicate.side_effect = mock_communicate
   144      mock_process.returncode = 0
   145      mock_subprocess.return_value = mock_process
   146      main()
   147      assert mock_subprocess.call_args_list == [
   148          call(['zypper', '--version'], stdout=-1, stderr=-1, env=ANY),
   149          call(
   150              ['zypper', '--userdata', 'skuba-update', 'ref', '-s'],
   151              stdout=None, stderr=None, env=ANY
   152          ),
   153          call([
   154              'zypper', '--userdata', 'skuba-update', '--non-interactive',
   155              '--non-interactive-include-reboot-patches', 'patch'
   156          ], stdout=None, stderr=None, env=ANY),
   157          call(
   158              ['zypper', '--userdata', 'skuba-update', 'ps', '-sss'],
   159              stdout=-1, stderr=-1, env=ANY
   160          ),
   161          call(
   162              ['systemctl', 'restart', 'some_service1'],
   163              stdout=None, stderr=None, env=ANY
   164          ),
   165          call(
   166              ['systemctl', 'restart', 'some_service2'],
   167              stdout=None, stderr=None, env=ANY
   168          ),
   169          call(
   170              ['zypper', '--userdata', 'skuba-update', 'needs-rebooting'],
   171              stdout=None, stderr=None, env=ANY
   172          ),
   173      ]
   174  
   175  
   176  @patch('subprocess.Popen')
   177  @patch('skuba_update.skuba_update.run_zypper_command')
   178  def test_restart_services_error(mock_zypp_cmd, mock_subprocess, capsys):
   179      command_type = namedtuple(
   180          'command', ['output', 'error', 'returncode']
   181      )
   182  
   183      mock_process = Mock()
   184      mock_process.communicate.return_value = (b'', b'restart error msg')
   185      mock_process.returncode = 1
   186      mock_subprocess.return_value = mock_process
   187  
   188      mock_zypp_cmd.return_value = command_type(
   189          output="service1\nservice2",
   190          error='',
   191          returncode=0
   192      )
   193  
   194      restart_services()
   195      out, err = capsys.readouterr()
   196      assert 'returned non zero exit code' in out
   197  
   198  
   199  @patch('skuba_update.skuba_update.node_name_from_machine_id')
   200  @patch('skuba_update.skuba_update.annotate_updates_available')
   201  @patch('argparse.ArgumentParser.parse_args')
   202  @patch('os.environ.get', new={}.get, spec_set=True)
   203  @patch('os.geteuid')
   204  @patch('subprocess.Popen')
   205  def test_main_annotate_only(
   206          mock_subprocess, mock_geteuid, mock_args, mock_annotate, mock_name
   207  ):
   208      args = Mock()
   209      args.annotate_only = True
   210      mock_args.return_value = args
   211      mock_geteuid.return_value = 0
   212      mock_process = Mock()
   213      mock_process.communicate.return_value = (b'zypper 1.14.15', b'stderr')
   214      mock_process.returncode = ZYPPER_EXIT_INF_UPDATE_NEEDED
   215      mock_subprocess.return_value = mock_process
   216      main()
   217      assert mock_subprocess.call_args_list == [
   218          call(['zypper', '--version'], stdout=-1, stderr=-1, env=ANY),
   219          call(
   220              ['zypper', '--userdata', 'skuba-update', 'ref', '-s'],
   221              stdout=None, stderr=None, env=ANY
   222          ),
   223          call([
   224              'rpm', '-q', 'caasp-release', '--queryformat', '%{VERSION}'
   225          ], stdout=-1, stderr=-1, env=ANY),
   226      ]
   227  
   228  
   229  @patch('skuba_update.skuba_update.node_name_from_machine_id')
   230  @patch('skuba_update.skuba_update.annotate_updates_available')
   231  @patch('argparse.ArgumentParser.parse_args')
   232  @patch('os.environ.get', new={}.get, spec_set=True)
   233  @patch('os.geteuid')
   234  @patch('subprocess.Popen')
   235  def test_main_zypper_returns_100(
   236          mock_subprocess, mock_geteuid, mock_args, mock_annotate, mock_name
   237  ):
   238      return_values = [(b'', b''), (b'zypper 1.14.15', b'')]
   239  
   240      def mock_communicate():
   241          if len(return_values) > 1:
   242              return return_values.pop()
   243          else:
   244              return return_values[0]
   245  
   246      args = Mock()
   247      args.annotate_only = False
   248      mock_args.return_value = args
   249      mock_geteuid.return_value = 0
   250      mock_process = Mock()
   251      mock_process.communicate.side_effect = mock_communicate
   252      mock_process.returncode = ZYPPER_EXIT_INF_RESTART_NEEDED
   253      mock_subprocess.return_value = mock_process
   254      main()
   255      assert mock_subprocess.call_args_list == [
   256          call(['zypper', '--version'], stdout=-1, stderr=-1, env=ANY),
   257          call([
   258              'zypper', '--userdata', 'skuba-update', 'ref', '-s'
   259          ], stdout=None, stderr=None, env=ANY),
   260          call([
   261              'zypper', '--userdata', 'skuba-update', '--non-interactive',
   262              '--non-interactive-include-reboot-patches', 'patch'
   263          ], stdout=None, stderr=None, env=ANY),
   264          call([
   265              'zypper', '--userdata', 'skuba-update', '--non-interactive',
   266              '--non-interactive-include-reboot-patches', 'patch'
   267          ], stdout=None, stderr=None, env=ANY),
   268          call(
   269              ['zypper', '--userdata', 'skuba-update', 'ps', '-sss'],
   270              stdout=-1, stderr=-1, env=ANY
   271          ),
   272          call([
   273              'rpm', '-q', 'caasp-release', '--queryformat', '%{VERSION}'
   274          ], stdout=-1, stderr=-1, env=ANY),
   275          call([
   276              'zypper', '--userdata', 'skuba-update', 'needs-rebooting'
   277          ], stdout=None, stderr=None, env=ANY),
   278      ]
   279  
   280  
   281  @patch('pathlib.Path.is_file')
   282  @patch('subprocess.Popen')
   283  def test_update_zypper_is_fine_but_created_reboot_required(
   284          mock_subprocess, mock_is_file
   285  ):
   286      mock_process = Mock()
   287      mock_process.communicate.return_value = (b'stdout', b'stderr')
   288  
   289      mock_process.returncode = ZYPPER_EXIT_INF_REBOOT_NEEDED
   290      mock_subprocess.return_value = mock_process
   291      mock_is_file.return_value = True
   292  
   293      exception = False
   294      try:
   295          reboot_sentinel_file(update())
   296      except PermissionError as e:
   297          exception = True
   298          msg = 'Permission denied: \'{0}\''.format(REBOOT_REQUIRED_PATH)
   299          assert msg in str(e)
   300      assert exception
   301  
   302  
   303  @patch('subprocess.Popen')
   304  def test_run_zypper_command(mock_subprocess):
   305      mock_process = Mock()
   306      mock_process.communicate.return_value = (b'stdout', b'stderr')
   307      mock_process.returncode = 0
   308      mock_subprocess.return_value = mock_process
   309      assert run_zypper_command(['patch']) == 0
   310      mock_process.returncode = ZYPPER_EXIT_INF_RESTART_NEEDED
   311      mock_subprocess.return_value = mock_process
   312      assert run_zypper_command(
   313          ['patch']) == ZYPPER_EXIT_INF_RESTART_NEEDED
   314  
   315  
   316  @patch('subprocess.Popen')
   317  def test_run_zypper_command_failure(mock_subprocess):
   318      mock_process = Mock()
   319      mock_process.communicate.return_value = (b'', b'')
   320      mock_process.returncode = 1
   321      mock_subprocess.return_value = mock_process
   322      exception = False
   323      try:
   324          run_zypper_command(['patch']) == 'stdout'
   325      except Exception as e:
   326          exception = True
   327          assert '"zypper --userdata skuba-update patch" failed' in str(e)
   328      assert exception
   329  
   330  
   331  @patch('builtins.open',
   332         mock_open(read_data='9ea12911449eb7b5f8f228294bf9209a'))
   333  @patch('subprocess.Popen')
   334  @patch('json.loads')
   335  def test_node_name_from_machine_id(mock_loads, mock_subprocess):
   336      json_node_object = {
   337          'items': [
   338              {
   339                  'metadata': {
   340                      'name': 'my-node-1'
   341                  },
   342                  'status': {
   343                      'nodeInfo': {
   344                          'machineID': '49f8e2911a1449b7b5ef2bf92282909a'
   345                      }
   346                  }
   347              },
   348              {
   349                  'metadata': {
   350                      'name': 'my-node-2'
   351                  },
   352                  'status': {
   353                      'nodeInfo': {
   354                          'machineID': '9ea12911449eb7b5f8f228294bf9209a'
   355                      }
   356                  }
   357              }
   358          ]
   359      }
   360      breaking_json_node_object = {'Items': []}
   361  
   362      mock_process = Mock()
   363      mock_process.communicate.return_value = (json.dumps(json_node_object)
   364                                               .encode(), b'')
   365      mock_process.returncode = 0
   366      mock_subprocess.return_value = mock_process
   367      mock_loads.return_value = json_node_object
   368      assert node_name_from_machine_id() == 'my-node-2'
   369  
   370      json_node_object2 = json_node_object
   371      json_node_object2['items'][1]['status']['nodeInfo']['machineID'] = \
   372          'another-id-that-doesnt-reflect-a-node'
   373      mock_loads.return_value = json_node_object2
   374      exception = False
   375      try:
   376          node_name_from_machine_id() == 'my-node-2'
   377      except Exception as e:
   378          exception = True
   379          assert 'Node name could not be determined' in str(e)
   380      assert exception
   381  
   382      mock_loads.return_value = breaking_json_node_object
   383      exception = False
   384      try:
   385          node_name_from_machine_id() == 'my-node-2'
   386      except Exception as e:
   387          exception = True
   388          assert 'Unexpected format' in str(e)
   389      assert exception
   390      exception = False
   391      mock_process.returncode = 1
   392      try:
   393          node_name_from_machine_id() == 'my-node'
   394      except Exception as e:
   395          exception = True
   396          assert 'Kubectl failed getting nodes list' in str(e)
   397      assert exception
   398  
   399  
   400  @patch('subprocess.Popen')
   401  def test_annotate(mock_subprocess, capsys):
   402      mock_process = Mock()
   403      mock_process.communicate.return_value = (b'node/my-node-1 annotated',
   404                                               b'stderr')
   405      mock_process.returncode = 0
   406      mock_subprocess.return_value = mock_process
   407      assert annotate(
   408          'node', 'my-node-1',
   409          KUBE_DISRUPTIVE_UPDATES_KEY, 'yes'
   410      ) == 'node/my-node-1 annotated'
   411      mock_process.returncode = 1
   412      annotate(
   413          'node', 'my-node-1',
   414          KUBE_DISRUPTIVE_UPDATES_KEY, 'yes'
   415      )
   416      out, err = capsys.readouterr()
   417      assert 'Warning! kubectl returned non zero exit code' in out
   418  
   419  
   420  @patch('skuba_update.skuba_update.node_name_from_machine_id')
   421  @patch('skuba_update.skuba_update.annotate')
   422  @patch('subprocess.Popen')
   423  def test_annotate_updates_empty(mock_subprocess, mock_annotate, mock_name):
   424      mock_name.return_value = 'mynode'
   425      mock_process = Mock()
   426      mock_process.communicate.return_value = (
   427          b'<stream><update-status><update-list>'
   428          b'</update-list></update-status></stream>', b''
   429      )
   430      mock_process.returncode = 0
   431      mock_subprocess.return_value = mock_process
   432      annotate_updates_available(mock_name.return_value)
   433      assert mock_subprocess.call_args_list == [
   434          call(
   435              ['zypper', '--userdata', 'skuba-update',
   436               '--non-interactive', '--xmlout', 'list-patches'],
   437              stdout=-1, stderr=-1, env=ANY
   438          )
   439      ]
   440      assert mock_annotate.call_args_list == [
   441          call('node', 'mynode', KUBE_UPDATES_KEY, 'no'),
   442          call('node', 'mynode', KUBE_SECURITY_UPDATES_KEY, 'no'),
   443          call('node', 'mynode', KUBE_DISRUPTIVE_UPDATES_KEY, 'no')
   444      ]
   445  
   446  
   447  @patch('skuba_update.skuba_update.node_name_from_machine_id')
   448  @patch('skuba_update.skuba_update.annotate')
   449  @patch('subprocess.Popen')
   450  def test_annotate_updates(mock_subprocess, mock_annotate, mock_name):
   451      mock_name.return_value = 'mynode'
   452      mock_process = Mock()
   453      mock_process.communicate.return_value = (
   454          b'<stream><update-status><update-list><update interactive="message">'
   455          b'</update></update-list></update-status></stream>', b''
   456      )
   457      mock_process.returncode = 0
   458      mock_subprocess.return_value = mock_process
   459      annotate_updates_available(mock_name.return_value)
   460      assert mock_subprocess.call_args_list == [
   461          call(
   462              ['zypper', '--userdata', 'skuba-update',
   463               '--non-interactive', '--xmlout', 'list-patches'],
   464              stdout=-1, stderr=-1, env=ANY
   465          )
   466      ]
   467      assert mock_annotate.call_args_list == [
   468          call('node', 'mynode', KUBE_UPDATES_KEY, 'yes'),
   469          call('node', 'mynode', KUBE_SECURITY_UPDATES_KEY, 'no'),
   470          call('node', 'mynode', KUBE_DISRUPTIVE_UPDATES_KEY, 'yes')
   471      ]
   472  
   473  
   474  @patch("skuba_update.skuba_update.node_name_from_machine_id")
   475  @patch("builtins.open", read_data="aa59dc0c5fe84247a77c26780dd0b3fd")
   476  @patch('subprocess.Popen')
   477  def test_annotate_updates_available(mock_subprocess, mock_open, mock_name):
   478      mock_name.return_value = 'mynode'
   479  
   480      mock_process = Mock()
   481      mock_process.communicate.return_value = (
   482          b'<stream><update-status><update-list><update interactive="message">'
   483          b'</update></update-list></update-status></stream>', b''
   484      )
   485      mock_process.returncode = 0
   486      mock_subprocess.return_value = mock_process
   487  
   488      annotate_updates_available(mock_name.return_value)
   489  
   490      assert mock_subprocess.call_args_list == [
   491          call(
   492              ['zypper', '--userdata', 'skuba-update',
   493               '--non-interactive', '--xmlout', 'list-patches'],
   494              stdout=-1, stderr=-1, env=ANY
   495          ),
   496          call(
   497              ["kubectl", "annotate", "--overwrite", "node",
   498               "mynode", "caasp.suse.com/has-updates=yes"],
   499              stdout=-1, stderr=-1, env=ANY
   500          ),
   501          call(
   502              ["kubectl", "annotate", "--overwrite", "node",
   503               "mynode", "caasp.suse.com/has-security-updates=no"],
   504              stdout=-1, stderr=-1, env=ANY
   505          ),
   506          call(
   507              ["kubectl", "annotate", "--overwrite", "node",
   508               "mynode", "caasp.suse.com/has-disruptive-updates=yes"],
   509              stdout=-1, stderr=-1, env=ANY
   510          )
   511      ]
   512  
   513  
   514  @patch('skuba_update.skuba_update.node_name_from_machine_id')
   515  @patch('skuba_update.skuba_update.annotate')
   516  @patch('subprocess.Popen')
   517  def test_annotate_updates_bad_xml(mock_subprocess, mock_annotate, mock_name):
   518      mock_name.return_value = 'mynode'
   519      mock_process = Mock()
   520      mock_process.communicate.return_value = (
   521          b'<update-status><update-list><update interactive="message">'
   522          b'</update></update-list></update-status>', b''
   523      )
   524      mock_process.returncode = 0
   525      mock_subprocess.return_value = mock_process
   526  
   527      annotate_updates_available(mock_name.return_value)
   528      assert mock_subprocess.call_args_list == [
   529          call(
   530              ['zypper', '--userdata', 'skuba-update',
   531               '--non-interactive', '--xmlout', 'list-patches'],
   532              stdout=-1, stderr=-1, env=ANY
   533          )
   534      ]
   535      assert mock_annotate.call_args_list == [
   536          call('node', 'mynode', KUBE_UPDATES_KEY, 'no'),
   537          call('node', 'mynode', KUBE_SECURITY_UPDATES_KEY, 'no'),
   538          call('node', 'mynode', KUBE_DISRUPTIVE_UPDATES_KEY, 'no')
   539      ]
   540  
   541  
   542  @patch('skuba_update.skuba_update.node_name_from_machine_id')
   543  @patch('skuba_update.skuba_update.annotate')
   544  @patch('subprocess.Popen')
   545  def test_annotate_updates_security(
   546          mock_subprocess, mock_annotate, mock_name
   547  ):
   548      mock_name.return_value = 'mynode'
   549      mock_process = Mock()
   550      mock_process.communicate.return_value = (
   551          b'<stream><update-status><update-list>'
   552          b'<update interactive="false" category="security">'
   553          b'</update></update-list></update-status></stream>', b''
   554      )
   555      mock_process.returncode = 0
   556      mock_subprocess.return_value = mock_process
   557  
   558      annotate_updates_available(mock_name.return_value)
   559      assert mock_subprocess.call_args_list == [
   560          call(
   561              ['zypper', '--userdata', 'skuba-update',
   562               '--non-interactive', '--xmlout', 'list-patches'],
   563              stdout=-1, stderr=-1, env=ANY
   564          )
   565      ]
   566      assert mock_annotate.call_args_list == [
   567          call('node', 'mynode', KUBE_UPDATES_KEY, 'yes'),
   568          call('node', 'mynode', KUBE_SECURITY_UPDATES_KEY, 'yes'),
   569          call('node', 'mynode', KUBE_DISRUPTIVE_UPDATES_KEY, 'no')
   570      ]
   571  
   572  
   573  @patch('skuba_update.skuba_update.node_name_from_machine_id')
   574  @patch('skuba_update.skuba_update.annotate')
   575  @patch('subprocess.Popen')
   576  def test_annotate_updates_available_is_reboot(
   577          mock_subprocess, mock_annotate, mock_name
   578  ):
   579      mock_name.return_value = 'mynode'
   580  
   581      mock_process = Mock()
   582      mock_process.communicate.return_value = (
   583          b'<stream><update-status><update-list><update interactive="reboot">'
   584          b'</update></update-list></update-status></stream>', b''
   585      )
   586      mock_process.returncode = 0
   587      mock_subprocess.return_value = mock_process
   588  
   589      annotate_updates_available(mock_name.return_value)
   590      assert mock_subprocess.call_args_list == [
   591          call(
   592              ['zypper', '--userdata', 'skuba-update',
   593               '--non-interactive', '--xmlout', 'list-patches'],
   594              stdout=-1, stderr=-1, env=ANY
   595          )
   596      ]
   597      assert mock_annotate.call_args_list == [
   598          call('node', 'mynode', KUBE_UPDATES_KEY, 'yes'),
   599          call('node', 'mynode', KUBE_SECURITY_UPDATES_KEY, 'no'),
   600          call('node', 'mynode', KUBE_DISRUPTIVE_UPDATES_KEY, 'yes')
   601      ]
   602  
   603  
   604  @patch('skuba_update.skuba_update.node_name_from_machine_id')
   605  @patch('skuba_update.skuba_update.annotate')
   606  @patch('subprocess.Popen')
   607  def test_annotate_caasp_release_version(
   608      mock_subprocess, mock_annotate, mock_name
   609  ):
   610      mock_name.return_value = 'mynode'
   611  
   612      mock_process = Mock()
   613      mock_process.communicate.return_value = (
   614          b'1.2.3', b''
   615      )
   616      mock_process.returncode = 0
   617      mock_subprocess.return_value = mock_process
   618  
   619      annotate_caasp_release_version(mock_name.return_value)
   620      assert mock_subprocess.call_args_list == [
   621          call(
   622              ['rpm', '-q', 'caasp-release', '--queryformat', '%{VERSION}'],
   623              stdout=-1, stderr=-1, env=ANY
   624          )
   625      ]
   626      assert mock_annotate.call_args_list == [
   627          call('node', 'mynode', KUBE_CAASP_RELEASE_VERSION_KEY, '1.2.3'),
   628      ]
   629  
   630  
   631  @patch('subprocess.Popen')
   632  def test_is_reboot_needed_truthy(mock_subprocess):
   633      mock_process = Mock()
   634      mock_process.communicate.return_value = (b'', b'')
   635      mock_process.returncode = ZYPPER_EXIT_INF_REBOOT_NEEDED
   636      mock_subprocess.return_value = mock_process
   637  
   638      assert is_reboot_needed()
   639  
   640  
   641  @patch('subprocess.Popen')
   642  def test_is_reboot_needed_falsey(mock_subprocess):
   643      mock_process = Mock()
   644      mock_process.communicate.return_value = (b'', b'')
   645      mock_process.returncode = ZYPPER_EXIT_INF_RESTART_NEEDED
   646      mock_subprocess.return_value = mock_process
   647  
   648      assert not is_reboot_needed()
   649  
   650  
   651  def test_get_update_list_bad_xml():
   652      assert get_update_list('<xml') is None