1module Control.Distributed.Process.Backend.Azure
5 , defaultAzureParameters
7 -- * Re-exports from Azure Service Management
11 -- * Remote and local processes
22import System.Environment (getEnv)
23import System.FilePath ((</>), takeFileName)
24import System.Environment.Executable (getExecutablePath)
25import System.IO (stdout, hFlush)
26import Data.Binary (encode, decode)
27import Data.Digest.Pure.MD5 (md5, MD5Digest)
28import qualified Data.ByteString as BSS
34import qualified Data.ByteString.Char8 as BSSC (pack)
35import qualified Data.ByteString.Lazy as BSL
43import qualified Data.ByteString.Lazy.Char8 as BSLC (unpack)
44import Data.Typeable (Typeable)
45import Control.Applicative ((<$>))
46import Control.Monad (void, unless)
47import Control.Monad.Reader (MonadReader, ReaderT, runReaderT, ask)
48import Control.Exception (Exception, catches, Handler(Handler))
49import Control.Monad.IO.Class (MonadIO, liftIO)
52import Network.Azure.ServiceManagement
58import qualified Network.Azure.ServiceManagement as Azure
65import qualified Network.SSH.Client.LibSSH2 as SSH
74import qualified Network.SSH.Client.LibSSH2.Foreign as SSH
81import qualified Network.SSH.Client.LibSSH2.Errors as SSH
88import Control.Distributed.Process (Process, Closure)
89import Control.Distributed.Process.Serializable (Serializable)
90import Control.Distributed.Process.Internal.Types
94import Network.Transport.Internal (encodeInt32, decodeInt32, prependLength)
97data Backend = Backend {
98 -- | Find virtual machines
99 findVMs :: IO [VirtualMachine]
100 -- | Copy the executable to a virtual machine
101 , copyToVM :: VirtualMachine -> IO ()
102 -- | Check the MD5 hash of the remote executable
103 , checkMD5 :: VirtualMachine -> IO Bool
104 -- | @runOnVM dict vm port p@ starts a CH node on port 'port' and runs 'p'
105 , callOnVM :: forall a.
110 -- | Create a new CH node and run the specified process in the background.
111 -- The CH node will exit when the process exists.
112 , spawnOnVM :: VirtualMachine
114 -> Closure (Backend -> Process ())
116 } deriving (Typeable)
118data AzureParameters = AzureParameters {
119 azureSetup :: AzureSetup
120 , azureSshUserName :: FilePath
121 , azureSshPublicKey :: FilePath
122 , azureSshPrivateKey :: FilePath
123 , azureSshPassphrase :: String
124 , azureSshKnownHosts :: FilePath
125 , azureSshRemotePath :: FilePath
126 , azureSshLocalPath :: FilePath
129-- | Create default azure parameters
130defaultAzureParameters :: String -- ^ Azure subscription ID
131 -> FilePath -- ^ Path to X509 certificate
132 -> FilePath -- ^ Path to private key
133 -> IO AzureParameters
134defaultAzureParameters sid x509 pkey = do
135 home <- getEnv "HOME"
136 user <- getEnv "USER"
137 self <- getExecutablePath
138 setup <- Azure.azureSetup sid x509 pkey
139 return AzureParameters
141 , azureSshUserName = user
142 , azureSshPublicKey = home </> ".ssh" </> "id_rsa.pub"
143 , azureSshPrivateKey = home </> ".ssh" </> "id_rsa"
144 , azureSshPassphrase = ""
145 , azureSshKnownHosts = home </> ".ssh" </> "known_hosts"
146 , azureSshRemotePath = takeFileName self
147 , azureSshLocalPath = self
150-- | Initialize the backend
151initializeBackend :: AzureParameters -- ^ Connection parameters
152 -> String -- ^ Cloud service name
154initializeBackend params cloudService =
156 findVMs = apiFindVMs params cloudService
157 , copyToVM = apiCopyToVM params
158 , checkMD5 = apiCheckMD5 params
159 , callOnVM = apiCallOnVM params
160 , spawnOnVM = apiSpawnOnVM params
163-- | Find virtual machines
164apiFindVMs :: AzureParameters -> String -> IO [VirtualMachine]
165apiFindVMs params cloudService = do
166 css <- Azure.cloudServices (azureSetup params)
167 case filter ((== cloudService) . cloudServiceName) css of
168 [cs] -> return $ cloudServiceVMs cs
171-- | Start a CH node on the given virtual machine
172apiCopyToVM :: AzureParameters -> VirtualMachine -> IO ()
173apiCopyToVM params vm =
174 void . withSSH2 params vm $ \s -> catchSshError s $
175 SSH.scpSendFile s 0o700 (azureSshLocalPath params) (azureSshRemotePath params)
177-- | Call a process on a VM
178apiCallOnVM :: AzureParameters
183apiCallOnVM params vm port ppair =
184 withSSH2 params vm $ \s -> do
185 let exe = "PATH=. " ++ azureSshRemotePath params
187 ++ " --host " ++ vmIpAddress vm
188 ++ " --port " ++ port
190 (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do
191 SSH.channelExecute ch exe
192 _ <- SSH.writeChannel ch (encodeInt32 (BSL.length rprocEnc))
193 _ <- SSH.writeAllChannel ch rprocEnc
194 runLocalProcess (ppairLocal ppair) ch
197 else error "callOnVM: Non-zero exit status"
199 rprocEnc :: BSL.ByteString
200 rprocEnc = encode (ppairRemote ppair)
202apiSpawnOnVM :: AzureParameters
205 -> Closure (Backend -> Process ())
207apiSpawnOnVM params vm port proc =
208 withSSH2 params vm $ \s -> do
209 let exe = "PATH=. " ++ azureSshRemotePath params
211 ++ " --host " ++ vmIpAddress vm
212 ++ " --port " ++ port
215 (status, r) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do
216 SSH.channelExecute ch exe
217 _ <- SSH.writeChannel ch (encodeInt32 (BSL.length procEnc))
218 _ <- SSH.writeAllChannel ch procEnc
219 SSH.channelSendEOF ch
220 SSH.readAllChannel ch
221 unless (status == 0) $ error (BSLC.unpack r)
223 procEnc :: BSL.ByteString
224 procEnc = encode proc
226-- | Check the MD5 hash of the executable on the remote machine
227apiCheckMD5 :: AzureParameters -> VirtualMachine -> IO Bool
228apiCheckMD5 params vm = do
229 hash <- localHash params
230 withSSH2 params vm $ \s -> do
231 (r, _) <- SSH.withChannelBy (SSH.openChannelSession s) id $ \ch -> do
232 SSH.channelExecute ch "md5sum -c --status"
233 SSH.writeChannel ch . BSSC.pack $ show hash ++ " " ++ azureSshRemotePath params
234 SSH.channelSendEOF ch
235 SSH.readAllChannel ch
238withSSH2 :: AzureParameters -> VirtualMachine -> (SSH.Session -> IO a) -> IO a
239withSSH2 params (Azure.vmSshEndpoint -> Just ep) =
240 SSH.withSSH2 (azureSshKnownHosts params)
241 (azureSshPublicKey params)
242 (azureSshPrivateKey params)
243 (azureSshPassphrase params)
244 (azureSshUserName params)
246 (read $ endpointPort ep)
248 error $ "withSSH2: No SSH endpoint for virtual machine " ++ vmName vm
250catchSshError :: SSH.Session -> IO a -> IO a
252 catches io [ Handler handleErrorCode
253 , Handler handleNullPointer
256 handleErrorCode :: SSH.ErrorCode -> IO a
257 handleErrorCode _ = do
258 (_, str) <- SSH.getLastError s
261 handleNullPointer :: SSH.NULL_POINTER -> IO a
262 handleNullPointer _ = do
263 (_, str) <- SSH.getLastError s
266localHash :: AzureParameters -> IO MD5Digest
267localHash params = md5 <$> BSL.readFile (azureSshLocalPath params)
269--------------------------------------------------------------------------------
270-- Local and remote processes --
271--------------------------------------------------------------------------------
273data ProcessPair a = ProcessPair {
274 ppairRemote :: RemoteProcess ()
275 , ppairLocal :: LocalProcess a
278type RemoteProcess a = Closure (Backend -> Process a)
280newtype LocalProcess a = LocalProcess { unLocalProcess :: ReaderT SSH.Channel IO a }
281 deriving (Functor, Monad, MonadIO, MonadReader SSH.Channel)
283runLocalProcess :: LocalProcess a -> SSH.Channel -> IO a
284runLocalProcess = runReaderT . unLocalProcess
286localExpect :: Serializable a => LocalProcess a
287localExpect = LocalProcess $ do
290 isE <- readIntChannel ch
291 len <- readIntChannel ch
292 msg <- readSizeChannel ch len
294 then error (decode msg)
295 else return (decode msg)
297localSend :: Serializable a => a -> LocalProcess ()
298localSend x = LocalProcess $ do
300 liftIO $ mapM_ (SSH.writeChannel ch)
306remoteSend :: Serializable a => a -> Process ()
307remoteSend = liftIO . remoteSend' 0
309remoteThrow :: Exception e => e -> Process ()
310remoteThrow = liftIO . remoteSend' 1 . show
312remoteSend' :: Serializable a => Int -> a -> IO ()
313remoteSend' flags x = do
315 BSS.hPut stdout (encodeInt32 flags)
316 BSS.hPut stdout (encodeInt32 (BSL.length enc))
320--------------------------------------------------------------------------------
322--------------------------------------------------------------------------------
324readSizeChannel :: SSH.Channel -> Int -> IO BSL.ByteString
325readSizeChannel ch = go []
327 go :: [BSS.ByteString] -> Int -> IO BSL.ByteString
328 go acc 0 = return (BSL.fromChunks $ reverse acc)
330 bs <- SSH.readChannel ch (fromIntegral (0x400 `min` size))
331 go (bs : acc) (size - BSS.length bs)
333readIntChannel :: SSH.Channel -> IO Int
335 decodeInt32 . BSS.concat . BSL.toChunks <$> readSizeChannel ch 4
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list