Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port #51625 to master #56953

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/ref/modules/all/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ execution modules
smbios
smf_service
smtp
snap
snapper
solaris_fmadm
solaris_group
Expand Down
6 changes: 6 additions & 0 deletions doc/ref/modules/all/salt.modules.snap.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
=================
salt.modules.snap
=================

.. automodule:: salt.modules.snap
:members:
1 change: 1 addition & 0 deletions doc/ref/states/all/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ state modules
slack
smartos
smtp
snap
snapper
solrcloud
splunk
Expand Down
6 changes: 6 additions & 0 deletions doc/ref/states/all/salt.states.snap.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
================
salt.states.snap
================

.. automodule:: salt.states.snap
:members:
129 changes: 129 additions & 0 deletions salt/modules/snap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
"""
Manage snap packages via Salt

:depends: snapd for distribution

"""

from __future__ import absolute_import, print_function, unicode_literals

import subprocess

import salt.utils.path

SNAP_BINARY_NAME = "snap"

__virtualname__ = "snap"


def __virtual__():
if salt.utils.path.which("snap"):
return __virtualname__

return (
False,
'The snap execution module cannot be loaded: the "snap" binary is not in the path.',
)


def install(pkg, channel=None, refresh=False):
"""
Install the specified snap package from the specified channel.
Returns a dictionary of "result" and "output".

pkg
The snap package name

channel
Optional. The snap channel to install from, eg "beta"

refresh : False
If True, use "snap refresh" instead of "snap install".
This allows changing the channel of a previously installed package.
"""
args = []
ret = {"result": None, "output": ""}

if refresh:
cmd = "refresh"
else:
cmd = "install"

if channel:
args.append("--channel=" + channel)

try:
# Try to run it, merging stderr into output
ret["output"] = subprocess.check_output(
[SNAP_BINARY_NAME, cmd, pkg] + args, stderr=subprocess.STDOUT
)
ret["result"] = True
except subprocess.CalledProcessError as e:
ret["output"] = e.output
ret["result"] = False

return ret


def is_installed(pkg):
"""
Returns True if there is any version of the specified package installed.

pkg
The package name
"""
return bool(versions_installed(pkg))


def remove(pkg):
"""
Remove the specified snap package. Returns a dictionary of "result" and "output".

pkg
The package name
"""
ret = {"result": None, "output": ""}
try:
ret["output"] = subprocess.check_output([SNAP_BINARY_NAME, "remove", pkg])
ret["result"] = True
except subprocess.CalledProcessError as e:
ret["output"] = e.output
ret["result"] = False


# Parse 'snap list' into a dict
def versions_installed(pkg):
"""
Query which version(s) of the specified snap package are installed.
Returns a list of 0 or more dictionaries.

pkg
The package name
"""

try:
# Try to run it, merging stderr into output
output = subprocess.check_output(
[SNAP_BINARY_NAME, "list", pkg], stderr=subprocess.STDOUT
)
except subprocess.CalledProcessError:
return []

lines = output.splitlines()[1:]
ret = []
for item in lines:
# If fields contain spaces this will break.
i = item.split()
# Ignore 'Notes' field
ret.append(
{
"name": i[0],
"version": i[1],
"rev": i[2],
"tracking": i[3],
"publisher": i[4],
}
)

return ret
125 changes: 125 additions & 0 deletions salt/states/snap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
"""
Management of snap packages
===============================================
"""
from __future__ import absolute_import, print_function, unicode_literals

import salt.utils.path

__virtualname__ = "snap"


def __virtual__():
if salt.utils.path.which("snap"):
return __virtualname__

return (
False,
'The snap state module cannot be loaded: the "snap" binary is not in the path.',
)


def installed(name, channel=None):
"""
Ensure that the named snap package is installed

name
The snap package

channel
Optional. The channel to install the package from.
"""
ret = {"name": name, "changes": {}, "pchanges": {}, "result": None, "comment": ""}

old = __salt__["snap.versions_installed"](name)
if not old:
if __opts__["test"]:
ret["comment"] = 'Package "{0}" would have been installed'.format(name)
ret["pchanges"]["new"] = name
ret["pchanges"]["old"] = None
ret["result"] = None
return ret

install = __salt__["snap.install"](name, channel=channel)
if install["result"]:
ret["comment"] = 'Package "{0}" was installed'.format(name)
ret["changes"]["new"] = name
ret["changes"]["old"] = None
ret["result"] = True
return ret

ret["comment"] = 'Package "{0}" failed to install'.format(name)
ret["comment"] += "\noutput:\n" + install["output"]
ret["result"] = False
return ret

# Currently snap always returns only one line?
old_channel = old[0]["tracking"]
if old_channel != channel and channel is not None:
if __opts__["test"]:
ret[
"comment"
] = 'Package "{0}" would have been switched to channel {1}'.format(
name, channel
)
ret["pchanges"]["old_channel"] = old_channel
ret["pchanges"]["new_channel"] = channel
ret["result"] = None
return ret

refresh = __salt__["snap.install"](name, channel=channel, refresh=True)
if refresh["result"]:
ret["comment"] = 'Package "{0}" was switched to channel {1}'.format(
name, channel
)
ret["pchanges"]["old_channel"] = old_channel
ret["pchanges"]["new_channel"] = channel
ret["result"] = True
return ret

ret["comment"] = 'Failed to switch Package "{0}" to channel {1}'.format(
name, channel
)
ret["comment"] += "\noutput:\n" + install["output"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been testing out using these modules in the packages-formula, to replace the use of cmd.run for snap installations. Hit a traceback with this line:

An exception occurred in this state: Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/salt/state.py", line 2179, in call
    ret = self.states[cdata["full"]](
  File "/usr/lib/python3/dist-packages/salt/loader/lazy.py", line 149, in __call__
    return self.loader.run(run_func, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/salt/loader/lazy.py", line 1201, in run
    return self._last_context.run(self._run_as, _func_or_method, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/salt/loader/lazy.py", line 1216, in _run_as
    return _func_or_method(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/salt/loader/lazy.py", line 1249, in wrapper
    return f(*args, **kwargs)
  File "/var/cache/salt/minion/extmods/states/snap.py", line 84, in installed
    ret["comment"] += "\noutput:\n" + install["output"]
UnboundLocalError: local variable 'install' referenced before assignment

Looking at the code, realised that this line should actually be:

Suggested change
ret["comment"] += "\noutput:\n" + install["output"]
ret["comment"] += "\noutput:\n" + refresh["output"]

Left over from a copy-paste of L53 above, perhaps.

@lordcirth As the original PR author, would you mind confirming this suggestion?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@myii Sounds correct to me

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate the response, @lordcirth. I've got another question, if you don't mind.

I've found that the execution module functions are returning bytes instead of string objects (where relevant). Should __salt__["cmd.run_all"]() be used instead of subprocess.check_output()? Checking across other modules, that seems to be the main method being used when working with command output.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not very experienced with Python or Saltstack internals. Your guess is probably better than mine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lordcirth OK, If I get around to pushing this to the packages-formula, I'll try to refactor it accordingly. I'll report back here on how things work out. Thanks again for the prompt response -- and for providing these modules in the first place!

ret["result"] = False
return ret

ret["comment"] = 'Package "{0}" is already installed'.format(name)
if __opts__["test"]:
ret["result"] = None
return ret

ret["result"] = True
return ret


def removed(name):
"""
Ensure that the named snap package is not installed

name
The snap package
"""

ret = {"name": name, "changes": {}, "pchanges": {}, "result": None, "comment": ""}

old = __salt__["snap.versions_installed"](name)
if not old:
ret["comment"] = "Package {0} is not installed".format(name)
ret["result"] = True
return ret

if __opts__["test"]:
ret["comment"] = "Package {0} would have been removed".format(name)
ret["result"] = None
ret["pchanges"]["old"] = old[0]["version"]
ret["pchanges"]["new"] = None
return ret

remove = __salt__["snap.remove"](name)
ret["comment"] = "Package {0} removed".format(name)
ret["result"] = True
ret["changes"]["old"] = old[0]["version"]
ret["changes"]["new"] = None
return ret
Loading