rsync.net configurator tested and 100% working
This commit is contained in:
parent
b43580ac6f
commit
46fd6b54c7
2 changed files with 118 additions and 49 deletions
|
@ -25,9 +25,11 @@ import Yesod
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Map as M
|
import qualified Data.Map as M
|
||||||
|
import qualified Control.Exception as E
|
||||||
import Network.BSD
|
import Network.BSD
|
||||||
import System.Posix.User
|
import System.Posix.User
|
||||||
import System.Process (CreateProcess(..))
|
import System.Process (CreateProcess(..))
|
||||||
|
import Control.Concurrent
|
||||||
|
|
||||||
sshConfigurator :: Widget -> Handler RepHtml
|
sshConfigurator :: Widget -> Handler RepHtml
|
||||||
sshConfigurator a = bootstrap (Just Config) $ do
|
sshConfigurator a = bootstrap (Just Config) $ do
|
||||||
|
@ -42,6 +44,18 @@ data SshServer = SshServer
|
||||||
}
|
}
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
|
{- SshServer is only used for applicative form prompting, this converts
|
||||||
|
- the result of such a form into a SshData. -}
|
||||||
|
mkSshData :: SshServer -> SshData
|
||||||
|
mkSshData sshserver = SshData
|
||||||
|
{ sshHostName = fromMaybe "" $ hostname sshserver
|
||||||
|
, sshUserName = username sshserver
|
||||||
|
, sshDirectory = fromMaybe "" $ directory sshserver
|
||||||
|
, sshRepoName = genSshRepoName sshserver
|
||||||
|
, needsPubKey = False
|
||||||
|
, rsyncOnly = False
|
||||||
|
}
|
||||||
|
|
||||||
sshServerAForm :: (Maybe Text) -> AForm WebApp WebApp SshServer
|
sshServerAForm :: (Maybe Text) -> AForm WebApp WebApp SshServer
|
||||||
sshServerAForm localusername = SshServer
|
sshServerAForm localusername = SshServer
|
||||||
<$> aopt check_hostname "Host name" Nothing
|
<$> aopt check_hostname "Host name" Nothing
|
||||||
|
@ -89,12 +103,8 @@ getAddSshR = sshConfigurator $ do
|
||||||
(status, needspubkey) <- liftIO $ testServer sshserver
|
(status, needspubkey) <- liftIO $ testServer sshserver
|
||||||
if usable status
|
if usable status
|
||||||
then lift $ redirect $ ConfirmSshR $
|
then lift $ redirect $ ConfirmSshR $
|
||||||
SshData
|
(mkSshData sshserver)
|
||||||
{ sshHostName = fromJust $ hostname sshserver
|
{ needsPubKey = needspubkey
|
||||||
, sshUserName = username sshserver
|
|
||||||
, sshDirectory = fromMaybe "" $ directory sshserver
|
|
||||||
, sshRepoName = genSshRepoName sshserver
|
|
||||||
, needsPubKey = needspubkey
|
|
||||||
, rsyncOnly = (status == UsableRsyncServer)
|
, rsyncOnly = (status == UsableRsyncServer)
|
||||||
}
|
}
|
||||||
else showform form enctype status
|
else showform form enctype status
|
||||||
|
@ -112,36 +122,36 @@ getAddSshR = sshConfigurator $ do
|
||||||
- a special ssh key will need to be generated just for this server.
|
- a special ssh key will need to be generated just for this server.
|
||||||
-
|
-
|
||||||
- Once logged into the server, probe to see if git-annex-shell is
|
- Once logged into the server, probe to see if git-annex-shell is
|
||||||
- available, or rsync.
|
- available, or rsync.\
|
||||||
-}
|
-}
|
||||||
testServer :: SshServer -> IO (ServerStatus, Bool)
|
testServer :: SshServer -> IO (ServerStatus, Bool)
|
||||||
testServer (SshServer { hostname = Nothing }) = return
|
testServer (SshServer { hostname = Nothing }) = return
|
||||||
(UnusableServer "Please enter a host name.", False)
|
(UnusableServer "Please enter a host name.", False)
|
||||||
testServer sshserver = do
|
testServer sshserver = do
|
||||||
status <- probe sshserver [sshopt "NumberOfPasswordPrompts" "0"]
|
status <- probe [sshOpt "NumberOfPasswordPrompts" "0"]
|
||||||
if usable status
|
if usable status
|
||||||
then return (status, False)
|
then return (status, False)
|
||||||
else do
|
else do
|
||||||
status' <- probe sshserver []
|
status' <- probe []
|
||||||
return (status', True)
|
return (status', True)
|
||||||
where
|
where
|
||||||
probe s extraopts = do
|
probe extraopts = do
|
||||||
knownhost <- knownHost sshserver
|
|
||||||
let remotecommand = join ";" $
|
let remotecommand = join ";" $
|
||||||
[ report "loggedin"
|
[ report "loggedin"
|
||||||
, checkcommand "git-annex-shell"
|
, checkcommand "git-annex-shell"
|
||||||
, checkcommand "rsync"
|
, checkcommand "rsync"
|
||||||
]
|
]
|
||||||
let sshopts = nonempty $ extraopts ++
|
knownhost <- knownHost sshserver
|
||||||
|
let sshopts = filter (not . null) $ extraopts ++
|
||||||
{- If this is an already known host, let
|
{- If this is an already known host, let
|
||||||
- ssh check it as usual.
|
- ssh check it as usual.
|
||||||
- Otherwise, trust the host key. -}
|
- Otherwise, trust the host key. -}
|
||||||
[ if knownhost then "" else sshopt "StrictHostKeyChecking" "no"
|
[ if knownhost then "" else sshOpt "StrictHostKeyChecking" "no"
|
||||||
, "-n" -- don't read from stdin
|
, "-n" -- don't read from stdin
|
||||||
, genSshHost (fromJust $ hostname s) (username s)
|
, genSshHost (fromJust $ hostname sshserver) (username sshserver)
|
||||||
, remotecommand
|
, remotecommand
|
||||||
]
|
]
|
||||||
parsetranscript . fst <$> sshTranscript sshopts
|
parsetranscript . fst <$> sshTranscript sshopts ""
|
||||||
parsetranscript s
|
parsetranscript s
|
||||||
| reported "git-annex-shell" = UsableSshServer
|
| reported "git-annex-shell" = UsableSshServer
|
||||||
| reported "rsync" = UsableRsyncServer
|
| reported "rsync" = UsableRsyncServer
|
||||||
|
@ -151,11 +161,13 @@ testServer sshserver = do
|
||||||
"Failed to ssh to the server. Transcript: " ++ s
|
"Failed to ssh to the server. Transcript: " ++ s
|
||||||
where
|
where
|
||||||
reported r = token r `isInfixOf` s
|
reported r = token r `isInfixOf` s
|
||||||
nonempty = filter $ not . null
|
|
||||||
checkcommand c = "if which " ++ c ++ "; then " ++ report c ++ "; fi"
|
checkcommand c = "if which " ++ c ++ "; then " ++ report c ++ "; fi"
|
||||||
token r = "git-annex-probe " ++ r
|
token r = "git-annex-probe " ++ r
|
||||||
report r = "echo " ++ token r
|
report r = "echo " ++ token r
|
||||||
sshopt k v = concat ["-o", k, "=", v]
|
|
||||||
|
{- ssh -ofoo=bar command-line option -}
|
||||||
|
sshOpt :: String -> String -> String
|
||||||
|
sshOpt k v = concat ["-o", k, "=", v]
|
||||||
|
|
||||||
sshDir :: IO FilePath
|
sshDir :: IO FilePath
|
||||||
sshDir = do
|
sshDir = do
|
||||||
|
@ -172,23 +184,49 @@ genSshRepoName s = (T.unpack $ fromJust $ hostname s) ++
|
||||||
(maybe "" (\d -> '_' : T.unpack d) (directory s))
|
(maybe "" (\d -> '_' : T.unpack d) (directory s))
|
||||||
|
|
||||||
{- The output of ssh, including both stdout and stderr. -}
|
{- The output of ssh, including both stdout and stderr. -}
|
||||||
sshTranscript :: [String] -> IO (String, Bool)
|
sshTranscript :: [String] -> String -> IO (String, Bool)
|
||||||
sshTranscript opts = do
|
sshTranscript opts input = do
|
||||||
(readf, writef) <- createPipe
|
(readf, writef) <- createPipe
|
||||||
readh <- fdToHandle readf
|
readh <- fdToHandle readf
|
||||||
writeh <- fdToHandle writef
|
writeh <- fdToHandle writef
|
||||||
(_, _, _, pid) <- createProcess $
|
(Just inh, _, _, pid) <- createProcess $
|
||||||
(proc "ssh" opts)
|
(proc "ssh" opts)
|
||||||
{ std_in = Inherit
|
{ std_in = CreatePipe
|
||||||
, std_out = UseHandle writeh
|
, std_out = UseHandle writeh
|
||||||
, std_err = UseHandle writeh
|
, std_err = UseHandle writeh
|
||||||
}
|
}
|
||||||
hClose writeh
|
hClose writeh
|
||||||
transcript <- hGetContentsStrict readh
|
|
||||||
|
-- fork off a thread to start consuming the output
|
||||||
|
transcript <- hGetContents readh
|
||||||
|
outMVar <- newEmptyMVar
|
||||||
|
_ <- forkIO $ E.evaluate (length transcript) >> putMVar outMVar ()
|
||||||
|
|
||||||
|
-- now write and flush any input
|
||||||
|
when (not (null input)) $ do hPutStr inh input; hFlush inh
|
||||||
|
hClose inh -- done with stdin
|
||||||
|
|
||||||
|
-- wait on the output
|
||||||
|
takeMVar outMVar
|
||||||
hClose readh
|
hClose readh
|
||||||
|
|
||||||
ok <- checkSuccessProcess pid
|
ok <- checkSuccessProcess pid
|
||||||
|
return ()
|
||||||
return (transcript, ok)
|
return (transcript, ok)
|
||||||
|
|
||||||
|
{- Runs a ssh command; if it fails shows the user the transcript,
|
||||||
|
- and if it succeeds, runs an action. -}
|
||||||
|
sshSetup :: [String] -> String -> Handler RepHtml -> Handler RepHtml
|
||||||
|
sshSetup opts input a = do
|
||||||
|
(transcript, ok) <- liftIO $ sshTranscript opts input
|
||||||
|
if ok
|
||||||
|
then a
|
||||||
|
else showSshErr transcript
|
||||||
|
|
||||||
|
showSshErr :: String -> Handler RepHtml
|
||||||
|
showSshErr msg = sshConfigurator $
|
||||||
|
$(widgetFile "configurators/makessherror")
|
||||||
|
|
||||||
{- Does ssh have known_hosts data for a hostname? -}
|
{- Does ssh have known_hosts data for a hostname? -}
|
||||||
knownHost :: SshServer -> IO Bool
|
knownHost :: SshServer -> IO Bool
|
||||||
knownHost (SshServer { hostname = Nothing }) = return False
|
knownHost (SshServer { hostname = Nothing }) = return False
|
||||||
|
@ -218,14 +256,9 @@ makeSsh rsync sshdata
|
||||||
| otherwise = makeSsh' rsync sshdata Nothing
|
| otherwise = makeSsh' rsync sshdata Nothing
|
||||||
|
|
||||||
makeSsh' :: Bool -> SshData -> Maybe String -> Handler RepHtml
|
makeSsh' :: Bool -> SshData -> Maybe String -> Handler RepHtml
|
||||||
makeSsh' rsync sshdata pubkey = do
|
makeSsh' rsync sshdata pubkey =
|
||||||
(transcript, ok) <- liftIO $ sshTranscript [sshhost, remoteCommand]
|
sshSetup [sshhost, remoteCommand] "" $
|
||||||
if ok
|
makeSshRepo rsync sshdata
|
||||||
then do
|
|
||||||
r <- runAnnex undefined makerepo
|
|
||||||
syncRemote r
|
|
||||||
redirect RepositoriesR
|
|
||||||
else showerr transcript
|
|
||||||
where
|
where
|
||||||
sshhost = genSshHost (sshHostName sshdata) (sshUserName sshdata)
|
sshhost = genSshHost (sshHostName sshdata) (sshUserName sshdata)
|
||||||
remotedir = T.unpack $ sshDirectory sshdata
|
remotedir = T.unpack $ sshDirectory sshdata
|
||||||
|
@ -236,22 +269,29 @@ makeSsh' rsync sshdata pubkey = do
|
||||||
, if rsync then Nothing else Just $ "git annex init"
|
, if rsync then Nothing else Just $ "git annex init"
|
||||||
, maybe Nothing (makeAuthorizedKeys sshdata) pubkey
|
, maybe Nothing (makeAuthorizedKeys sshdata) pubkey
|
||||||
]
|
]
|
||||||
showerr msg = sshConfigurator $
|
|
||||||
$(widgetFile "configurators/makessherror")
|
makeSshRepo :: Bool -> SshData -> Handler RepHtml
|
||||||
{- This is a one-sided remote setup; the remote server
|
makeSshRepo forcersync sshdata = do
|
||||||
- probably does not have a route to the client here. -}
|
r <- runAnnex undefined $
|
||||||
makerepo = addRemote $
|
addRemote $ maker (sshRepoName sshdata) sshurl
|
||||||
(if rsync then makeRsyncRemote else makeGitRemote)
|
syncRemote r
|
||||||
(sshRepoName sshdata) sshurl
|
redirect RepositoriesR
|
||||||
sshurl = T.unpack $ T.concat $ if rsync
|
where
|
||||||
then [u, h, ":", sshDirectory sshdata, "/"]
|
rsync = forcersync || rsyncOnly sshdata
|
||||||
else ["ssh://", u, h, d, "/"]
|
maker
|
||||||
|
| rsync = makeRsyncRemote
|
||||||
|
| otherwise = makeGitRemote
|
||||||
|
sshurl = T.unpack $ T.concat $
|
||||||
|
if rsync
|
||||||
|
then [u, h, ":", sshDirectory sshdata, "/"]
|
||||||
|
else ["ssh://", u, h, d, "/"]
|
||||||
where
|
where
|
||||||
u = maybe "" (\v -> T.concat [v, "@"]) $ sshUserName sshdata
|
u = maybe "" (\v -> T.concat [v, "@"]) $ sshUserName sshdata
|
||||||
h = sshHostName sshdata
|
h = sshHostName sshdata
|
||||||
d
|
d
|
||||||
| "/" `T.isPrefixOf` sshDirectory sshdata = d
|
| "/" `T.isPrefixOf` sshDirectory sshdata = d
|
||||||
| otherwise = T.concat ["/~/", sshDirectory sshdata]
|
| otherwise = T.concat ["/~/", sshDirectory sshdata]
|
||||||
|
|
||||||
|
|
||||||
{- Inits a rsync special remote, and returns the name of the remote. -}
|
{- Inits a rsync special remote, and returns the name of the remote. -}
|
||||||
makeRsyncRemote :: String -> String -> Annex String
|
makeRsyncRemote :: String -> String -> Annex String
|
||||||
|
@ -322,19 +362,48 @@ genSshKey sshdata = do
|
||||||
user = maybe "" (\u -> "-" ++ T.unpack u) (sshUserName sshdata)
|
user = maybe "" (\u -> "-" ++ T.unpack u) (sshUserName sshdata)
|
||||||
|
|
||||||
getAddRsyncNetR :: Handler RepHtml
|
getAddRsyncNetR :: Handler RepHtml
|
||||||
getAddRsyncNetR = bootstrap (Just Config) $ do
|
getAddRsyncNetR = do
|
||||||
sideBarDisplay
|
((result, form), enctype) <- runFormGet $
|
||||||
setTitle "Add a Rsync.net repository"
|
renderBootstrap $ sshServerAForm Nothing
|
||||||
((result, form), enctype) <- lift $
|
let showform status = bootstrap (Just Config) $ do
|
||||||
runFormGet $ renderBootstrap $ sshServerAForm Nothing
|
sideBarDisplay
|
||||||
let showform status = do
|
setTitle "Add a Rsync.net repository"
|
||||||
let authtoken = webAppFormAuthToken
|
let authtoken = webAppFormAuthToken
|
||||||
$(widgetFile "configurators/addrsync.net")
|
$(widgetFile "configurators/addrsync.net")
|
||||||
case result of
|
case result of
|
||||||
FormSuccess sshserver -> do
|
FormSuccess sshserver -> do
|
||||||
|
knownhost <- liftIO $ knownHost sshserver
|
||||||
|
(pubkey, sshdata) <- liftIO $ genSshKey $
|
||||||
|
(mkSshData sshserver)
|
||||||
|
{ needsPubKey = True
|
||||||
|
, rsyncOnly = True
|
||||||
|
, sshRepoName = "rsync.net"
|
||||||
|
}
|
||||||
|
{- I'd prefer to separate commands with && , but
|
||||||
|
- rsync.net's shell does not support that.
|
||||||
|
-
|
||||||
|
- The dd method of appending to the
|
||||||
|
- authorized_keys file is the one recommended by
|
||||||
|
- rsync.net documentation. I touch the file first
|
||||||
|
- to not need to use a different method to create
|
||||||
|
- it.
|
||||||
|
-}
|
||||||
|
let remotecommand = join ";" $
|
||||||
|
[ "mkdir -p .ssh"
|
||||||
|
, "touch .ssh/authorized_keys"
|
||||||
|
, "dd of=.ssh/authorized_keys oflag=append conv=notrunc"
|
||||||
|
, "mkdir -p " ++ T.unpack (sshDirectory sshdata)
|
||||||
|
]
|
||||||
|
let sshopts = filter (not . null) $
|
||||||
|
[ if knownhost then "" else sshOpt "StrictHostKeyChecking" "no"
|
||||||
|
, genSshHost (sshHostName sshdata) (sshUserName sshdata)
|
||||||
|
, remotecommand
|
||||||
|
]
|
||||||
|
|
||||||
let host = fromMaybe "" $ hostname sshserver
|
let host = fromMaybe "" $ hostname sshserver
|
||||||
checkhost host showform $ do
|
checkhost host showform $
|
||||||
error "TODO"
|
sshSetup sshopts pubkey $
|
||||||
|
makeSshRepo True sshdata
|
||||||
_ -> showform UntestedServer
|
_ -> showform UntestedServer
|
||||||
where
|
where
|
||||||
checkhost host showform a
|
checkhost host showform a
|
||||||
|
|
|
@ -40,4 +40,4 @@
|
||||||
<p>
|
<p>
|
||||||
Setting up your rsync.net repository. This could take a minute.
|
Setting up your rsync.net repository. This could take a minute.
|
||||||
<p>
|
<p>
|
||||||
You may be prompted for your rsync.net password.
|
You may be prompted for your rsync.net ssh password.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue